diff options
Diffstat (limited to 'src/de/danoeh/antennapod/service')
6 files changed, 2315 insertions, 1797 deletions
diff --git a/src/de/danoeh/antennapod/service/PlaybackService.java b/src/de/danoeh/antennapod/service/PlaybackService.java index 409ac6b48..025bf7dc4 100644 --- a/src/de/danoeh/antennapod/service/PlaybackService.java +++ b/src/de/danoeh/antennapod/service/PlaybackService.java @@ -2,13 +2,8 @@ package de.danoeh.antennapod.service; import java.io.IOException; import java.util.Date; -import java.util.concurrent.Future; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; +import java.util.List; +import java.util.concurrent.*; import android.annotation.SuppressLint; import android.app.Notification; @@ -40,366 +35,434 @@ 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.FeedComponent; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedManager; -import de.danoeh.antennapod.feed.FeedMedia; -import de.danoeh.antennapod.feed.MediaType; +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 */ +/** + * 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; - 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; - - /** - * 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 AudioManager audioManager; - private ComponentName mediaButtonReceiver; - - private MediaPlayer player; + /** + * 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 Playable media; - - /** True if media should be streamed (Extracted from Intent Extra) . */ - private boolean shouldStream; - - /** True if service should prepare playback after it has been initialized */ - private boolean prepareImmediately; - private boolean startWhenPrepared; - private FeedManager manager; - 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 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(); - - 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); + 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() { + 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 { - return new Intent(context, AudioplayerActivity.class); + ((VideoPlayer) mp).setOnPreparedListener(videoPreparedListener); + ((VideoPlayer) mp) + .setOnCompletionListener(videoCompletionListener); + ((VideoPlayer) mp) + .setOnSeekCompleteListener(videoSeekCompleteListener); + ((VideoPlayer) mp).setOnErrorListener(videoErrorListener); + ((VideoPlayer) mp) + .setOnBufferingUpdateListener(videoBufferingUpdateListener); + ((VideoPlayer) mp).setOnInfoListener(videoInfoListener); } } - } - - /** - * 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); - manager = FeedManager.getInstance(); - 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"); - } - }); - player = createMediaPlayer(); - - 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)); - - } - - private MediaPlayer createMediaPlayer() { - return createMediaPlayer(new MediaPlayer()); - } - - private MediaPlayer createMediaPlayer(MediaPlayer mp) { - if (mp != null) { - mp.setOnPreparedListener(preparedListener); - mp.setOnCompletionListener(completionListener); - mp.setOnSeekCompleteListener(onSeekCompleteListener); - mp.setOnErrorListener(onErrorListener); - mp.setOnBufferingUpdateListener(onBufferingUpdateListener); - mp.setOnInfoListener(onInfoListener); - } 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); - 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 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 (AppConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Ducking..."); - audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_LOWER, 0); - 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; - } - } - } - }; - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - super.onStartCommand(intent, flags, startId); - - if (AppConfig.DEBUG) - Log.d(TAG, "OnStartCommand called"); - int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1); - if (keycode != -1) { - if (AppConfig.DEBUG) - Log.d(TAG, "Received media button event"); - handleKeycode(keycode); - } else { - - Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); - boolean playbackType = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, - true); - if (playable == null) { - Log.e(TAG, "Playable extra wasn't sent to the service"); - if (media == null) { - stopSelf(); - } - // Intent values appear to be valid - // check if already playing and playbackType is the same - } else if (media == null - || !playable.getIdentifier().equals(media.getIdentifier()) - || playbackType != shouldStream) { - pause(true, false); - player.reset(); - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); - if (media == null - || !playable.getIdentifier().equals( - media.getIdentifier())) { - media = playable; - } - - if (media != null) { - shouldStream = playbackType; - startWhenPrepared = intent.getBooleanExtra( - EXTRA_START_WHEN_PREPARED, false); - prepareImmediately = intent.getBooleanExtra( - EXTRA_PREPARE_IMMEDIATELY, false); - initMediaplayer(); - - } else { - Log.e(TAG, "Media is null"); - stopSelf(); - } - - } else if (media != null) { - if (status == PlayerStatus.PAUSED) { - play(); - } - - } else { - Log.w(TAG, "Something went wrong. Shutting down..."); - stopSelf(); - } - } - return Service.START_NOT_STICKY; - } + @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 (AppConfig.DEBUG) + Log.d(TAG, "Lost audio focus temporarily. Ducking..."); + audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, + AudioManager.ADJUST_LOWER, 0); + 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) { @@ -432,166 +495,181 @@ public class PlaybackService extends Service { pause(true, true); } break; + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: { + 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(); - } + case KeyEvent.KEYCODE_MEDIA_REWIND: { + seekDelta(-PlaybackController.DEFAULT_SEEK_DELTA); + break; + } } - } - /** 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.release(); - player = createMediaPlayer(); - status = PlayerStatus.STOPPED; - if (media != null) { - initMediaplayer(); - } - } + /** + * 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.release(); + 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. */ - private void initMediaplayer() { - if (AppConfig.DEBUG) - Log.d(TAG, "Setting up media player"); - try { - MediaType mediaType = media.getMediaType(); - 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(); - } - } + /** + * 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 @@ -613,181 +691,245 @@ public class PlaybackService extends Service { } } - private MediaPlayer.OnPreparedListener preparedListener = new MediaPlayer.OnPreparedListener() { - @Override - public void onPrepared(MediaPlayer mp) { - 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 MediaPlayer.OnSeekCompleteListener onSeekCompleteListener = new MediaPlayer.OnSeekCompleteListener() { - - @Override - public void onSeekComplete(MediaPlayer mp) { - if (status == PlayerStatus.SEEKING) { - setStatus(statusBeforeSeek); - } - - } - }; - - private MediaPlayer.OnInfoListener onInfoListener = new MediaPlayer.OnInfoListener() { - - @Override - public boolean onInfo(MediaPlayer mp, int what, int extra) { - 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 MediaPlayer.OnErrorListener onErrorListener = new MediaPlayer.OnErrorListener() { - private static final String TAG = "PlaybackService.onErrorListener"; - - @Override - public boolean onError(MediaPlayer mp, int what, int extra) { - Log.w(TAG, "An error has occured: " + what); - if (mp.isPlaying()) { - pause(true, true); - } - sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what); - setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING); - stopSelf(); - return true; - } - }; - - private MediaPlayer.OnCompletionListener completionListener = new MediaPlayer.OnCompletionListener() { - - @Override - public void onCompletion(MediaPlayer mp) { - endPlayback(true); - } - }; - - private MediaPlayer.OnBufferingUpdateListener onBufferingUpdateListener = new MediaPlayer.OnBufferingUpdateListener() { - - @Override - public void onBufferingUpdate(MediaPlayer mp, 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(); - ((FeedMedia) media).setPlaybackCompletionDate(new Date()); - manager.markItemRead(PlaybackService.this, item, true, true); - nextItem = manager.getQueueSuccessorOfItem(item); - isInQueue = media instanceof FeedMedia - && manager.isInQueue(((FeedMedia) media).getItem()); - if (isInQueue) { - manager.removeQueueItem(PlaybackService.this, item, true); - } - manager.addItemToPlaybackHistory(PlaybackService.this, item); - manager.setFeedMedia(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(); - } - - 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(); - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, - notificationCode); - } else { - sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); - stopSelf(); - } - } + 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) @@ -813,10 +955,10 @@ public class PlaybackService extends Service { /** * 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 reset + * @param reinit * is true if service should reinit after pausing if the media * file is being streamed */ @@ -825,14 +967,14 @@ public class PlaybackService extends Service { if (AppConfig.DEBUG) Log.d(TAG, "Pausing playback."); player.pause(); + cancelPositionSaver(); + saveCurrentPosition(); + setStatus(PlayerStatus.PAUSED); if (abandonFocus) { audioManager.abandonAudioFocus(audioFocusChangeListener); pausedBecauseOfTransientAudiofocusLoss = false; disableSleepTimer(); } - cancelPositionSaver(); - saveCurrentPosition(); - setStatus(PlayerStatus.PAUSED); stopWidgetUpdater(); stopForeground(true); if (shouldStream && reinit) { @@ -871,8 +1013,7 @@ public class PlaybackService extends Service { public void reinit() { player.reset(); player = createMediaPlayer(player); - prepareImmediately = false; - initMediaplayer(); + initMediaplayer(false); } @SuppressLint("NewApi") @@ -890,6 +1031,7 @@ public class PlaybackService extends Service { Log.d(TAG, "Resuming/Starting playback"); writePlaybackPreferences(); + setSpeed(Float.parseFloat(UserPreferences.getPlaybackSpeed())); player.start(); if (status != PlayerStatus.PAUSED) { player.seekTo((int) media.getPosition()); @@ -1075,7 +1217,7 @@ public class PlaybackService extends Service { /** * Seek a specific position from the current position - * + * * @param delta * offset from current position (positive or negative) * */ @@ -1233,16 +1375,20 @@ public class PlaybackService extends Service { isPlaying = true; } - 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); - i.putExtra("ListSize", manager.getQueueSize(false)); - i.putExtra("duration", media.getDuration()); - i.putExtra("position", media.getPosition()); - sendBroadcast(i); + 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); + } } /** @@ -1314,12 +1460,11 @@ public class PlaybackService extends Service { Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent"); if (media != null) { setStatus(PlayerStatus.STOPPED); - player.reset(); - endPlayback(false); + endPlayback(true); } } } - }; + }; /** Periodically saves the position of the media file */ class PositionSaver implements Runnable { @@ -1421,7 +1566,7 @@ public class PlaybackService extends Service { return media; } - public MediaPlayer getPlayer() { + public IPlayer getPlayer() { return player; } @@ -1434,6 +1579,53 @@ public class PlaybackService extends Service { 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 @@ -1520,4 +1712,16 @@ public class PlaybackService extends Service { } } + + 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/download/DownloadRequest.java b/src/de/danoeh/antennapod/service/download/DownloadRequest.java new file mode 100644 index 000000000..1f4e32e1b --- /dev/null +++ b/src/de/danoeh/antennapod/service/download/DownloadRequest.java @@ -0,0 +1,177 @@ +package de.danoeh.antennapod.service.download; + +import android.os.Parcel; +import android.os.Parcelable; + +public class DownloadRequest implements Parcelable { + + private final String destination; + private final String source; + private final String title; + private final long feedfileId; + private final int feedfileType; + + protected int progressPercent; + protected long soFar; + protected long size; + protected int statusMsg; + + public DownloadRequest(String destination, String source, String title, + long feedfileId, int feedfileType) { + if (destination == null) { + throw new IllegalArgumentException("Destination must not be null"); + } + if (source == null) { + throw new IllegalArgumentException("Source must not be null"); + } + if (title == null) { + throw new IllegalArgumentException("Title must not be null"); + } + + this.destination = destination; + this.source = source; + this.title = title; + this.feedfileId = feedfileId; + this.feedfileType = feedfileType; + } + + private DownloadRequest(Parcel in) { + destination = in.readString(); + source = in.readString(); + title = in.readString(); + feedfileId = in.readLong(); + feedfileType = in.readInt(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(destination); + dest.writeString(source); + dest.writeString(title); + dest.writeLong(feedfileId); + dest.writeInt(feedfileType); + } + + public static final Parcelable.Creator<DownloadRequest> CREATOR = new Parcelable.Creator<DownloadRequest>() { + public DownloadRequest createFromParcel(Parcel in) { + return new DownloadRequest(in); + } + + public DownloadRequest[] newArray(int size) { + return new DownloadRequest[size]; + } + }; + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + ((destination == null) ? 0 : destination.hashCode()); + result = prime * result + (int) (feedfileId ^ (feedfileId >>> 32)); + result = prime * result + feedfileType; + result = prime * result + progressPercent; + result = prime * result + (int) (size ^ (size >>> 32)); + result = prime * result + (int) (soFar ^ (soFar >>> 32)); + result = prime * result + ((source == null) ? 0 : source.hashCode()); + result = prime * result + statusMsg; + result = prime * result + ((title == null) ? 0 : title.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + DownloadRequest other = (DownloadRequest) obj; + if (destination == null) { + if (other.destination != null) + return false; + } else if (!destination.equals(other.destination)) + return false; + if (feedfileId != other.feedfileId) + return false; + if (feedfileType != other.feedfileType) + return false; + if (progressPercent != other.progressPercent) + return false; + if (size != other.size) + return false; + if (soFar != other.soFar) + return false; + if (source == null) { + if (other.source != null) + return false; + } else if (!source.equals(other.source)) + return false; + if (statusMsg != other.statusMsg) + return false; + if (title == null) { + if (other.title != null) + return false; + } else if (!title.equals(other.title)) + return false; + return true; + } + + public String getDestination() { + return destination; + } + + public String getSource() { + return source; + } + + public String getTitle() { + return title; + } + + public long getFeedfileId() { + return feedfileId; + } + + public int getFeedfileType() { + return feedfileType; + } + + public int getProgressPercent() { + return progressPercent; + } + + public void setProgressPercent(int progressPercent) { + this.progressPercent = progressPercent; + } + + public long getSoFar() { + return soFar; + } + + public void setSoFar(long soFar) { + this.soFar = soFar; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public int getStatusMsg() { + return statusMsg; + } + + public void setStatusMsg(int statusMsg) { + this.statusMsg = statusMsg; + } +} diff --git a/src/de/danoeh/antennapod/service/download/DownloadService.java b/src/de/danoeh/antennapod/service/download/DownloadService.java index e1230e170..4040c85a8 100644 --- a/src/de/danoeh/antennapod/service/download/DownloadService.java +++ b/src/de/danoeh/antennapod/service/download/DownloadService.java @@ -1,28 +1,17 @@ -/** - * Registers a DownloadReceiver and waits for all Downloads - * to complete, then stops - * */ - package de.danoeh.antennapod.service.download; import java.io.File; import java.io.IOException; -import java.lang.Thread.UncaughtExceptionHandler; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; import javax.xml.parsers.ParserConfigurationException; +import de.danoeh.antennapod.storage.*; import org.xml.sax.SAXException; import android.annotation.SuppressLint; @@ -41,8 +30,6 @@ import android.os.AsyncTask; import android.os.Binder; import android.os.Handler; import android.os.IBinder; -import android.os.Parcel; -import android.os.Parcelable; import android.support.v4.app.NotificationCompat; import android.util.Log; import android.webkit.URLUtil; @@ -50,904 +37,859 @@ import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.DownloadActivity; import de.danoeh.antennapod.activity.DownloadLogActivity; -import de.danoeh.antennapod.asynctask.DownloadStatus; import de.danoeh.antennapod.feed.EventDistributor; import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedFile; import de.danoeh.antennapod.feed.FeedImage; import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.feed.FeedMedia; -import de.danoeh.antennapod.storage.DownloadRequestException; -import de.danoeh.antennapod.storage.DownloadRequester; import de.danoeh.antennapod.syndication.handler.FeedHandler; import de.danoeh.antennapod.syndication.handler.UnsupportedFeedtypeException; import de.danoeh.antennapod.util.ChapterUtils; import de.danoeh.antennapod.util.DownloadError; import de.danoeh.antennapod.util.InvalidFeedException; +/** + * Manages the download of feedfiles in the app. Downloads can be enqueued viathe startService intent. + * The argument of the intent is an instance of DownloadRequest in the EXTRA_REQUEST field of + * the intent. + * After the downloads have finished, the downloaded object will be passed on to a specific handler, depending on the + * type of the feedfile. + */ public class DownloadService extends Service { - private static final String TAG = "DownloadService"; - - public static String ACTION_ALL_FEED_DOWNLOADS_COMPLETED = "action.de.danoeh.antennapod.storage.all_feed_downloads_completed"; - - public static final String ACTION_ENQUEUE_DOWNLOAD = "action.de.danoeh.antennapod.service.enqueueDownload"; - public static final String ACTION_CANCEL_DOWNLOAD = "action.de.danoeh.antennapod.service.cancelDownload"; - public static final String ACTION_CANCEL_ALL_DOWNLOADS = "action.de.danoeh.antennapod.service.cancelAllDownloads"; - - /** Extra for ACTION_CANCEL_DOWNLOAD */ - public static final String EXTRA_DOWNLOAD_URL = "downloadUrl"; - - /** - * Sent by the DownloadService when the content of the downloads list - * changes. - */ - public static final String ACTION_DOWNLOADS_CONTENT_CHANGED = "action.de.danoeh.antennapod.service.downloadsContentChanged"; - - public static final String EXTRA_DOWNLOAD_ID = "extra.de.danoeh.antennapod.service.download_id"; - - /** Extra for ACTION_ENQUEUE_DOWNLOAD intent. */ - public static final String EXTRA_REQUEST = "request"; - - private CopyOnWriteArrayList<DownloadStatus> completedDownloads; - - private ExecutorService syncExecutor; - private ExecutorService downloadExecutor; - /** Number of threads of downloadExecutor. */ - private static final int NUM_PARALLEL_DOWNLOADS = 4; - - private DownloadRequester requester; - private FeedManager manager; - private NotificationCompat.Builder notificationCompatBuilder; - private Notification.BigTextStyle notificationBuilder; - private int NOTIFICATION_ID = 2; - private int REPORT_ID = 3; - - private List<Downloader> downloads; - - /** Number of completed downloads which are currently being handled. */ - private volatile int downloadsBeingHandled; - - private volatile boolean shutdownInitiated = false; - /** True if service is running. */ - public static boolean isRunning = false; - - private Handler handler; - - private NotificationUpdater notificationUpdater; - private ScheduledFuture notificationUpdaterFuture; - private static final int SCHED_EX_POOL_SIZE = 1; - private ScheduledThreadPoolExecutor schedExecutor; - - private final IBinder mBinder = new LocalBinder(); - - public class LocalBinder extends Binder { - public DownloadService getService() { - return DownloadService.this; - } - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (intent.getParcelableExtra(EXTRA_REQUEST) != null) { - onDownloadQueued(intent); - } - return Service.START_NOT_STICKY; - } - - @SuppressLint("NewApi") - @Override - public void onCreate() { - if (AppConfig.DEBUG) - Log.d(TAG, "Service started"); - isRunning = true; - handler = new Handler(); - completedDownloads = new CopyOnWriteArrayList<DownloadStatus>( - new ArrayList<DownloadStatus>()); - downloads = new ArrayList<Downloader>(); - registerReceiver(downloadQueued, new IntentFilter( - ACTION_ENQUEUE_DOWNLOAD)); - - IntentFilter cancelDownloadReceiverFilter = new IntentFilter(); - cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_ALL_DOWNLOADS); - cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_DOWNLOAD); - registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter); - syncExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() { - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - t.setUncaughtExceptionHandler(new UncaughtExceptionHandler() { - - @Override - public void uncaughtException(Thread thread, Throwable ex) { - Log.e(TAG, "Thread exited with uncaught exception"); - ex.printStackTrace(); - downloadsBeingHandled -= 1; - queryDownloads(); - } - }); - return t; - } - }); - downloadExecutor = Executors.newFixedThreadPool(NUM_PARALLEL_DOWNLOADS, - new ThreadFactory() { - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }); - 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"); - } - }); - setupNotificationBuilders(); - manager = FeedManager.getInstance(); - requester = DownloadRequester.getInstance(); - } - - @Override - public IBinder onBind(Intent intent) { - return mBinder; - } - - @Override - public void onDestroy() { - if (AppConfig.DEBUG) - Log.d(TAG, "Service shutting down"); - isRunning = false; - unregisterReceiver(cancelDownloadReceiver); - unregisterReceiver(downloadQueued); - } - - @SuppressLint("NewApi") - private void setupNotificationBuilders() { - PendingIntent pIntent = PendingIntent.getActivity(this, 0, new Intent( - this, DownloadActivity.class), - PendingIntent.FLAG_UPDATE_CURRENT); - - Bitmap icon = BitmapFactory.decodeResource(getResources(), - R.drawable.stat_notify_sync); - - if (android.os.Build.VERSION.SDK_INT >= 16) { - notificationBuilder = new Notification.BigTextStyle( - new Notification.Builder(this).setOngoing(true) - .setContentIntent(pIntent).setLargeIcon(icon) - .setSmallIcon(R.drawable.stat_notify_sync)); - } else { - notificationCompatBuilder = new NotificationCompat.Builder(this) - .setOngoing(true).setContentIntent(pIntent) - .setLargeIcon(icon) - .setSmallIcon(R.drawable.stat_notify_sync); - } - if (AppConfig.DEBUG) - Log.d(TAG, "Notification set up"); - } - - /** - * Updates the contents of the service's notifications. Should be called - * before setupNotificationBuilders. - */ - @SuppressLint("NewApi") - private Notification updateNotifications() { - String contentTitle = getString(R.string.download_notification_title); - String downloadsLeft = requester.getNumberOfDownloads() - + getString(R.string.downloads_left); - if (android.os.Build.VERSION.SDK_INT >= 16) { - - if (notificationBuilder != null) { - - StringBuilder bigText = new StringBuilder(""); - for (int i = 0; i < downloads.size(); i++) { - Downloader downloader = downloads.get(i); - if (downloader.getStatus() != null) { - FeedFile f = downloader.getStatus().getFeedFile(); - if (f.getClass() == Feed.class) { - Feed feed = (Feed) f; - if (feed.getTitle() != null) { - if (i > 0) { - bigText.append("\n"); - } - bigText.append("\u2022 " + feed.getTitle()); - } - } else if (f.getClass() == FeedMedia.class) { - FeedMedia media = (FeedMedia) f; - if (media.getItem().getTitle() != null) { - if (i > 0) { - bigText.append("\n"); - } - bigText.append("\u2022 " - + media.getItem().getTitle() - + " (" - + downloader.getStatus() - .getProgressPercent() + "%)"); - } - } - } - } - notificationBuilder.setSummaryText(downloadsLeft); - notificationBuilder.setBigContentTitle(contentTitle); - if (bigText != null) { - notificationBuilder.bigText(bigText.toString()); - } - return notificationBuilder.build(); - } - } else { - if (notificationCompatBuilder != null) { - notificationCompatBuilder.setContentTitle(contentTitle); - notificationCompatBuilder.setContentText(downloadsLeft); - return notificationCompatBuilder.getNotification(); - } - } - return null; - } - - private Downloader getDownloader(String downloadUrl) { - for (Downloader downloader : downloads) { - if (downloader.getStatus().getFeedFile().getDownload_url() - .equals(downloadUrl)) { - return downloader; - } - } - return null; - } - - private BroadcastReceiver cancelDownloadReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(ACTION_CANCEL_DOWNLOAD)) { - String url = intent.getStringExtra(EXTRA_DOWNLOAD_URL); - if (url == null) { - throw new IllegalArgumentException( - "ACTION_CANCEL_DOWNLOAD intent needs download url extra"); - } - if (AppConfig.DEBUG) - Log.d(TAG, "Cancelling download with url " + url); - Downloader d = getDownloader(url); - if (d != null) { - d.cancel(); - removeDownload(d); - } else { - Log.e(TAG, "Could not cancel download with url " + url); - } - - } else if (intent.getAction().equals(ACTION_CANCEL_ALL_DOWNLOADS)) { - for (Downloader d : downloads) { - d.cancel(); - DownloadRequester.getInstance().removeDownload( - d.getStatus().getFeedFile()); - d.getStatus().getFeedFile().setFile_url(null); - if (AppConfig.DEBUG) - Log.d(TAG, "Cancelled all downloads"); - } - downloads.clear(); - sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); - - } - queryDownloads(); - } - - }; - - private void onDownloadQueued(Intent intent) { - if (AppConfig.DEBUG) - Log.d(TAG, "Received enqueue request"); - Request request = intent.getParcelableExtra(EXTRA_REQUEST); - if (request == null) { - throw new IllegalArgumentException( - "ACTION_ENQUEUE_DOWNLOAD intent needs request extra"); - } - if (shutdownInitiated) { - if (AppConfig.DEBUG) - Log.d(TAG, "Cancelling shutdown; new download was queued"); - shutdownInitiated = false; - } - - DownloadRequester requester = DownloadRequester.getInstance(); - FeedFile feedfile = requester.getDownload(request.source); - if (feedfile != null) { - - DownloadStatus status = new DownloadStatus(feedfile, - feedfile.getHumanReadableIdentifier()); - Downloader downloader = getDownloader(status); - if (downloader != null) { - downloads.add(downloader); - downloadExecutor.submit(downloader); - sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); - } - } else { - Log.e(TAG, - "Could not find feedfile in download requester when trying to enqueue new download"); - } - queryDownloads(); - } - - private BroadcastReceiver downloadQueued = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - onDownloadQueued(intent); - } - - }; - - private Downloader getDownloader(DownloadStatus status) { - if (URLUtil.isHttpUrl(status.getFeedFile().getDownload_url())) { - return new HttpDownloader(new DownloaderCallback() { - - @Override - public void onDownloadCompleted(final Downloader downloader) { - handler.post(new Runnable() { - - @Override - public void run() { - DownloadService.this - .onDownloadCompleted(downloader); - } - }); - } - }, status); - } - Log.e(TAG, "Could not find appropriate downloader for " - + status.getFeedFile().getDownload_url()); - return null; - } - - @SuppressLint("NewApi") - public void onDownloadCompleted(final Downloader downloader) { - final AsyncTask<Void, Void, Void> handlerTask = new AsyncTask<Void, Void, Void>() { - boolean successful; - - @Override - protected void onPostExecute(Void result) { - super.onPostExecute(result); - if (!successful) { - queryDownloads(); - } - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - removeDownload(downloader); - } - - @Override - protected Void doInBackground(Void... params) { - if (AppConfig.DEBUG) - Log.d(TAG, "Received 'Download Complete' - message."); - downloadsBeingHandled += 1; - DownloadStatus status = downloader.getStatus(); - status.setCompletionDate(new Date()); - successful = status.isSuccessful(); - - FeedFile download = status.getFeedFile(); - if (download != null) { - if (successful) { - if (download.getClass() == Feed.class) { - handleCompletedFeedDownload(status); - } else if (download.getClass() == FeedImage.class) { - handleCompletedImageDownload(status); - } else if (download.getClass() == FeedMedia.class) { - handleCompletedFeedMediaDownload(status); - } - } else { - download.setFile_url(null); - download.setDownloaded(false); - if (!successful && !status.isCancelled()) { - Log.e(TAG, "Download failed"); - saveDownloadStatus(status); - } - sendDownloadHandledIntent(); - downloadsBeingHandled -= 1; - } - } - return null; - } - }; - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - handlerTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } else { - handlerTask.execute(); - } - } - - /** - * Remove download from the DownloadRequester list and from the - * DownloadService list. - */ - private void removeDownload(final Downloader d) { - if (AppConfig.DEBUG) - Log.d(TAG, "Removing downloader: " - + d.getStatus().getFeedFile().getDownload_url()); - boolean rc = downloads.remove(d); - if (AppConfig.DEBUG) - Log.d(TAG, "Result of downloads.remove: " + rc); - DownloadRequester.getInstance().removeDownload( - d.getStatus().getFeedFile()); - sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); - } - - /** - * Adds a new DownloadStatus object to the list of completed downloads and - * saves it in the database - * - * @param status - * the download that is going to be saved - */ - private void saveDownloadStatus(DownloadStatus status) { - completedDownloads.add(status); - manager.addDownloadStatus(this, status); - } - - private void sendDownloadHandledIntent() { - EventDistributor.getInstance().sendDownloadHandledBroadcast(); - } - - /** - * Creates a notification at the end of the service lifecycle to notify the - * user about the number of completed downloads. A report will only be - * created if the number of successfully downloaded feeds is bigger than 1 - * or if there is at least one failed download which is not an image or if - * there is at least one downloaded media file. - */ - private void updateReport() { - // check if report should be created - boolean createReport = false; - int successfulDownloads = 0; - int failedDownloads = 0; - - // a download report is created if at least one download has failed - // (excluding failed image downloads) - for (DownloadStatus status : completedDownloads) { - if (status.isSuccessful()) { - successfulDownloads++; - } else if (!status.isCancelled()) { - if (status.getFeedFile().getClass() != FeedImage.class) { - createReport = true; - } - failedDownloads++; - } - } - - if (createReport) { - if (AppConfig.DEBUG) - Log.d(TAG, "Creating report"); - // create notification object - Notification notification = new NotificationCompat.Builder(this) - .setTicker( - getString(de.danoeh.antennapod.R.string.download_report_title)) - .setContentTitle( - getString(de.danoeh.antennapod.R.string.download_report_title)) - .setContentText( - String.format( - getString(R.string.download_report_content), - successfulDownloads, failedDownloads)) - .setSmallIcon(R.drawable.stat_notify_sync) - .setLargeIcon( - BitmapFactory.decodeResource(getResources(), - R.drawable.stat_notify_sync)) - .setContentIntent( - PendingIntent.getActivity(this, 0, new Intent(this, - DownloadLogActivity.class), 0)) - .setAutoCancel(true).getNotification(); - NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(REPORT_ID, notification); - } else { - if (AppConfig.DEBUG) - Log.d(TAG, "No report is created"); - } - completedDownloads.clear(); - } - - /** Check if there's something else to download, otherwise stop */ - void queryDownloads() { - int numOfDownloads = downloads.size(); - if (AppConfig.DEBUG) { - Log.d(TAG, numOfDownloads + " downloads left"); - Log.d(TAG, "Downloads being handled: " + downloadsBeingHandled); - Log.d(TAG, "ShutdownInitiated: " + shutdownInitiated); - } - - if (numOfDownloads == 0 && downloadsBeingHandled <= 0) { - if (AppConfig.DEBUG) - Log.d(TAG, "Starting shutdown"); - shutdownInitiated = true; - updateReport(); - cancelNotificationUpdater(); - stopForeground(true); - } else { - setupNotificationUpdater(); - startForeground(NOTIFICATION_ID, updateNotifications()); - } - } - - /** Is called whenever a Feed is downloaded */ - private void handleCompletedFeedDownload(DownloadStatus status) { - if (AppConfig.DEBUG) - Log.d(TAG, "Handling completed Feed Download"); - syncExecutor.execute(new FeedSyncThread(status)); - - } - - /** Is called whenever a Feed-Image is downloaded */ - private void handleCompletedImageDownload(DownloadStatus status) { - if (AppConfig.DEBUG) - Log.d(TAG, "Handling completed Image Download"); - syncExecutor.execute(new ImageHandlerThread(status)); - } - - /** Is called whenever a FeedMedia is downloaded. */ - private void handleCompletedFeedMediaDownload(DownloadStatus status) { - if (AppConfig.DEBUG) - Log.d(TAG, "Handling completed FeedMedia Download"); - syncExecutor.execute(new MediaHandlerThread(status)); - } - - /** - * Takes a single Feed, parses the corresponding file and refreshes - * information in the manager - */ - class FeedSyncThread implements Runnable { - private static final String TAG = "FeedSyncThread"; - - private Feed feed; - private DownloadStatus status; - - private int reason; - private boolean successful; - - public FeedSyncThread(DownloadStatus status) { - this.feed = (Feed) status.getFeedFile(); - this.status = status; - } - - public void run() { - Feed savedFeed = null; - reason = 0; - String reasonDetailed = null; - successful = true; - final FeedManager manager = FeedManager.getInstance(); - FeedHandler feedHandler = new FeedHandler(); - feed.setDownloaded(true); - - try { - feed = feedHandler.parseFeed(feed); - if (AppConfig.DEBUG) - Log.d(TAG, feed.getTitle() + " parsed"); - if (checkFeedData(feed) == false) { - throw new InvalidFeedException(); - } - // Save information of feed in DB - savedFeed = manager.updateFeed(DownloadService.this, feed); - // Download Feed Image if provided and not downloaded - if (savedFeed.getImage() != null - && savedFeed.getImage().isDownloaded() == false) { - if (AppConfig.DEBUG) - Log.d(TAG, "Feed has image; Downloading...."); - savedFeed.getImage().setFeed(savedFeed); - final Feed savedFeedRef = savedFeed; - handler.post(new Runnable() { - - @Override - public void run() { - try { - requester.downloadImage(DownloadService.this, - savedFeedRef.getImage()); - } catch (DownloadRequestException e) { - e.printStackTrace(); - manager.addDownloadStatus( - DownloadService.this, - new DownloadStatus( - savedFeedRef.getImage(), - savedFeedRef - .getImage() - .getHumanReadableIdentifier(), - DownloadError.ERROR_REQUEST_ERROR, - false, e.getMessage())); - } - } - }); - - } - - } catch (SAXException e) { - successful = false; - e.printStackTrace(); - reason = DownloadError.ERROR_PARSER_EXCEPTION; - reasonDetailed = e.getMessage(); - } catch (IOException e) { - successful = false; - e.printStackTrace(); - reason = DownloadError.ERROR_PARSER_EXCEPTION; - reasonDetailed = e.getMessage(); - } catch (ParserConfigurationException e) { - successful = false; - e.printStackTrace(); - reason = DownloadError.ERROR_PARSER_EXCEPTION; - reasonDetailed = e.getMessage(); - } catch (UnsupportedFeedtypeException e) { - e.printStackTrace(); - successful = false; - reason = DownloadError.ERROR_UNSUPPORTED_TYPE; - reasonDetailed = e.getMessage(); - } catch (InvalidFeedException e) { - e.printStackTrace(); - successful = false; - reason = DownloadError.ERROR_PARSER_EXCEPTION; - reasonDetailed = e.getMessage(); - } - - // cleanup(); - if (savedFeed == null) { - savedFeed = feed; - } - - saveDownloadStatus(new DownloadStatus(savedFeed, - savedFeed.getHumanReadableIdentifier(), reason, successful, - reasonDetailed)); - sendDownloadHandledIntent(); - downloadsBeingHandled -= 1; - handler.post(new Runnable() { - - @Override - public void run() { - queryDownloads(); - - } - }); - } - - /** Checks if the feed was parsed correctly. */ - private boolean checkFeedData(Feed feed) { - if (feed.getTitle() == null) { - Log.e(TAG, "Feed has no title."); - return false; - } - if (!hasValidFeedItems(feed)) { - Log.e(TAG, "Feed has invalid items"); - return false; - } - if (AppConfig.DEBUG) - Log.d(TAG, "Feed appears to be valid."); - return true; - - } - - private boolean hasValidFeedItems(Feed feed) { - for (FeedItem item : feed.getItemsArray()) { - if (item.getTitle() == null) { - Log.e(TAG, "Item has no title"); - return false; - } - if (item.getPubDate() == null) { - Log.e(TAG, - "Item has no pubDate. Using current time as pubDate"); - if (item.getTitle() != null) { - Log.e(TAG, "Title of invalid item: " + item.getTitle()); - } - item.setPubDate(new Date()); - } - } - return true; - } - - /** Delete files that aren't needed anymore */ - private void cleanup() { - if (feed.getFile_url() != null) { - if (new File(feed.getFile_url()).delete()) - if (AppConfig.DEBUG) - Log.d(TAG, "Successfully deleted cache file."); - else - Log.e(TAG, "Failed to delete cache file."); - feed.setFile_url(null); - } else if (AppConfig.DEBUG) { - Log.d(TAG, "Didn't delete cache file: File url is not set."); - } - } - - } - - /** Handles a completed image download. */ - class ImageHandlerThread implements Runnable { - private FeedImage image; - private DownloadStatus status; - - public ImageHandlerThread(DownloadStatus status) { - this.image = (FeedImage) status.getFeedFile(); - this.status = status; - } - - @Override - public void run() { - image.setDownloaded(true); - - saveDownloadStatus(status); - sendDownloadHandledIntent(); - manager.setFeedImage(DownloadService.this, image); - if (image.getFeed() != null) { - manager.setFeed(DownloadService.this, image.getFeed()); - } else { - Log.e(TAG, - "Image has no feed, image might not be saved correctly!"); - } - downloadsBeingHandled -= 1; - handler.post(new Runnable() { - - @Override - public void run() { - queryDownloads(); - - } - }); - } - } - - /** Handles a completed media download. */ - class MediaHandlerThread implements Runnable { - private FeedMedia media; - private DownloadStatus status; - - public MediaHandlerThread(DownloadStatus status) { - super(); - this.media = (FeedMedia) status.getFeedFile(); - this.status = status; - } - - @Override - public void run() { - boolean chaptersRead = false; - - media.setDownloaded(true); - // Get duration - MediaPlayer mediaplayer = new MediaPlayer(); - try { - mediaplayer.setDataSource(media.getFile_url()); - mediaplayer.prepare(); - media.setDuration(mediaplayer.getDuration()); - if (AppConfig.DEBUG) - Log.d(TAG, "Duration of file is " + media.getDuration()); - mediaplayer.reset(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - mediaplayer.release(); - } - - if (media.getItem().getChapters() == null) { - ChapterUtils.loadChaptersFromFileUrl(media); - if (media.getItem().getChapters() != null) { - chaptersRead = true; - } - } - - saveDownloadStatus(status); - sendDownloadHandledIntent(); - if (chaptersRead) { - manager.setFeedItem(DownloadService.this, media.getItem()); - } - manager.setFeedMedia(DownloadService.this, media); - - if (!FeedManager.getInstance().isInQueue(media.getItem())) { - FeedManager.getInstance().addQueueItem(DownloadService.this, - media.getItem()); - } - - downloadsBeingHandled -= 1; - handler.post(new Runnable() { - - @Override - public void run() { - queryDownloads(); - - } - }); - } - } - - /** Is used to request a new download. */ - public static class Request implements Parcelable { - private String destination; - private String source; - - public Request(String destination, String source) { - super(); - this.destination = destination; - this.source = source; - } - - private Request(Parcel in) { - destination = in.readString(); - source = in.readString(); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(destination); - dest.writeString(source); - } - - public static final Parcelable.Creator<Request> CREATOR = new Parcelable.Creator<Request>() { - public Request createFromParcel(Parcel in) { - return new Request(in); - } - - public Request[] newArray(int size) { - return new Request[size]; - } - }; - - public String getDestination() { - return destination; - } - - public String getSource() { - return source; - } - - } - - /** Schedules the notification updater task if it hasn't been scheduled yet. */ - private void setupNotificationUpdater() { - if (AppConfig.DEBUG) - Log.d(TAG, "Setting up notification updater"); - if (notificationUpdater == null) { - notificationUpdater = new NotificationUpdater(); - notificationUpdaterFuture = schedExecutor.scheduleAtFixedRate( - notificationUpdater, 5L, 5L, TimeUnit.SECONDS); - } - } - - private void cancelNotificationUpdater() { - boolean result = false; - if (notificationUpdaterFuture != null) { - result = notificationUpdaterFuture.cancel(true); - } - notificationUpdater = null; - notificationUpdaterFuture = null; - Log.d(TAG, "NotificationUpdater cancelled. Result: " + result); - } - - private class NotificationUpdater implements Runnable { - public void run() { - handler.post(new Runnable() { - @Override - public void run() { - Notification n = updateNotifications(); - if (n != null) { - NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(NOTIFICATION_ID, n); - } - } - }); - } - } - - public List<Downloader> getDownloads() { - return downloads; - } + private static final String TAG = "DownloadService"; + + /** + * Cancels one download. The intent MUST have an EXTRA_DOWNLOAD_URL extra that contains the download URL of the + * object whose download should be cancelled. + */ + public static final String ACTION_CANCEL_DOWNLOAD = "action.de.danoeh.antennapod.service.cancelDownload"; + + /** + * Cancels all running downloads. + */ + public static final String ACTION_CANCEL_ALL_DOWNLOADS = "action.de.danoeh.antennapod.service.cancelAllDownloads"; + + /** + * Extra for ACTION_CANCEL_DOWNLOAD + */ + public static final String EXTRA_DOWNLOAD_URL = "downloadUrl"; + + /** + * Sent by the DownloadService when the content of the downloads list + * changes. + */ + public static final String ACTION_DOWNLOADS_CONTENT_CHANGED = "action.de.danoeh.antennapod.service.downloadsContentChanged"; + + /** + * Extra for ACTION_ENQUEUE_DOWNLOAD intent. + */ + public static final String EXTRA_REQUEST = "request"; + + /** + * Stores DownloadStatus objects of completed downloads for creating a report at the end of the lifecylce. + */ + private List<DownloadStatus> completedDownloads; + + private ExecutorService syncExecutor; + private CompletionService<Downloader> downloadExecutor; + /** + * Number of threads of downloadExecutor. + */ + private static final int NUM_PARALLEL_DOWNLOADS = 4; + + private DownloadRequester requester; + + + private NotificationCompat.Builder notificationCompatBuilder; + private Notification.BigTextStyle notificationBuilder; + private int NOTIFICATION_ID = 2; + private int REPORT_ID = 3; + + /** + * Currently running downloads. + */ + private List<Downloader> downloads; + + /** + * Number of running downloads. + */ + private AtomicInteger numberOfDownloads; + + /** + * True if service is running. + */ + public static boolean isRunning = false; + + private Handler handler; + + private NotificationUpdater notificationUpdater; + private ScheduledFuture notificationUpdaterFuture; + private static final int SCHED_EX_POOL_SIZE = 1; + private ScheduledThreadPoolExecutor schedExecutor; + + private final IBinder mBinder = new LocalBinder(); + + public class LocalBinder extends Binder { + public DownloadService getService() { + return DownloadService.this; + } + } + + private Thread downloadCompletionThread = new Thread() { + private static final String TAG = "downloadCompletionThread"; + + @Override + public void run() { + if (AppConfig.DEBUG) Log.d(TAG, "downloadCompletionThread was started"); + while (!isInterrupted()) { + try { + Downloader downloader = downloadExecutor.take().get(); + if (AppConfig.DEBUG) + Log.d(TAG, "Received 'Download Complete' - message."); + removeDownload(downloader); + DownloadStatus status = downloader.getResult(); + boolean successful = status.isSuccessful(); + + final int type = status.getFeedfileType(); + if (successful) { + if (type == Feed.FEEDFILETYPE_FEED) { + handleCompletedFeedDownload(downloader + .getDownloadRequest()); + } else if (type == FeedImage.FEEDFILETYPE_FEEDIMAGE) { + handleCompletedImageDownload(status, downloader.getDownloadRequest()); + } else if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + handleCompletedFeedMediaDownload(status, downloader.getDownloadRequest()); + } + } else { + numberOfDownloads.decrementAndGet(); + if (!successful && !status.isCancelled()) { + Log.e(TAG, "Download failed"); + saveDownloadStatus(status); + } + sendDownloadHandledIntent(); + queryDownloadsAsync(); + } + } catch (InterruptedException e) { + if (AppConfig.DEBUG) Log.d(TAG, "DownloadCompletionThread was interrupted"); + } catch (ExecutionException e) { + e.printStackTrace(); + numberOfDownloads.decrementAndGet(); + } + } + if (AppConfig.DEBUG) Log.d(TAG, "End of downloadCompletionThread"); + } + }; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent.getParcelableExtra(EXTRA_REQUEST) != null) { + onDownloadQueued(intent); + } else if (numberOfDownloads.get() == 0) { + stopSelf(); + } + return Service.START_NOT_STICKY; + } + + @SuppressLint("NewApi") + @Override + public void onCreate() { + if (AppConfig.DEBUG) + Log.d(TAG, "Service started"); + isRunning = true; + handler = new Handler(); + completedDownloads = Collections.synchronizedList(new ArrayList<DownloadStatus>()); + downloads = new ArrayList<Downloader>(); + numberOfDownloads = new AtomicInteger(0); + + IntentFilter cancelDownloadReceiverFilter = new IntentFilter(); + cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_ALL_DOWNLOADS); + cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_DOWNLOAD); + registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter); + syncExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() { + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }); + downloadExecutor = new ExecutorCompletionService<Downloader>( + Executors.newFixedThreadPool(NUM_PARALLEL_DOWNLOADS, + new ThreadFactory() { + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + })); + 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"); + } + } + ); + downloadCompletionThread.start(); + setupNotificationBuilders(); + requester = DownloadRequester.getInstance(); + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + @Override + public void onDestroy() { + if (AppConfig.DEBUG) + Log.d(TAG, "Service shutting down"); + isRunning = false; + updateReport(); + + stopForeground(true); + NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.cancel(NOTIFICATION_ID); + + downloadCompletionThread.interrupt(); + syncExecutor.shutdown(); + schedExecutor.shutdown(); + cancelNotificationUpdater(); + unregisterReceiver(cancelDownloadReceiver); + } + + @SuppressLint("NewApi") + private void setupNotificationBuilders() { + PendingIntent pIntent = PendingIntent.getActivity(this, 0, new Intent( + this, DownloadActivity.class), + PendingIntent.FLAG_UPDATE_CURRENT); + + Bitmap icon = BitmapFactory.decodeResource(getResources(), + R.drawable.stat_notify_sync); + + if (android.os.Build.VERSION.SDK_INT >= 16) { + notificationBuilder = new Notification.BigTextStyle( + new Notification.Builder(this).setOngoing(true) + .setContentIntent(pIntent).setLargeIcon(icon) + .setSmallIcon(R.drawable.stat_notify_sync)); + } else { + notificationCompatBuilder = new NotificationCompat.Builder(this) + .setOngoing(true).setContentIntent(pIntent) + .setLargeIcon(icon) + .setSmallIcon(R.drawable.stat_notify_sync); + } + if (AppConfig.DEBUG) + Log.d(TAG, "Notification set up"); + } + + /** + * Updates the contents of the service's notifications. Should be called + * before setupNotificationBuilders. + */ + @SuppressLint("NewApi") + private Notification updateNotifications() { + String contentTitle = getString(R.string.download_notification_title); + String downloadsLeft = requester.getNumberOfDownloads() + + getString(R.string.downloads_left); + if (android.os.Build.VERSION.SDK_INT >= 16) { + + if (notificationBuilder != null) { + + StringBuilder bigText = new StringBuilder(""); + for (int i = 0; i < downloads.size(); i++) { + Downloader downloader = downloads.get(i); + final DownloadRequest request = downloader + .getDownloadRequest(); + if (request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { + if (request.getTitle() != null) { + if (i > 0) { + bigText.append("\n"); + } + bigText.append("\u2022 " + request.getTitle()); + } + } else if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + if (request.getTitle() != null) { + if (i > 0) { + bigText.append("\n"); + } + bigText.append("\u2022 " + request.getTitle() + + " (" + request.getProgressPercent() + + "%)"); + } + } + + } + notificationBuilder.setSummaryText(downloadsLeft); + notificationBuilder.setBigContentTitle(contentTitle); + if (bigText != null) { + notificationBuilder.bigText(bigText.toString()); + } + return notificationBuilder.build(); + } + } else { + if (notificationCompatBuilder != null) { + notificationCompatBuilder.setContentTitle(contentTitle); + notificationCompatBuilder.setContentText(downloadsLeft); + return notificationCompatBuilder.getNotification(); + } + } + return null; + } + + private Downloader getDownloader(String downloadUrl) { + for (Downloader downloader : downloads) { + if (downloader.getDownloadRequest().getSource().equals(downloadUrl)) { + return downloader; + } + } + return null; + } + + private BroadcastReceiver cancelDownloadReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(ACTION_CANCEL_DOWNLOAD)) { + String url = intent.getStringExtra(EXTRA_DOWNLOAD_URL); + if (url == null) { + throw new IllegalArgumentException( + "ACTION_CANCEL_DOWNLOAD intent needs download url extra"); + } + if (AppConfig.DEBUG) + Log.d(TAG, "Cancelling download with url " + url); + Downloader d = getDownloader(url); + if (d != null) { + d.cancel(); + } else { + Log.e(TAG, "Could not cancel download with url " + url); + } + + } else if (intent.getAction().equals(ACTION_CANCEL_ALL_DOWNLOADS)) { + for (Downloader d : downloads) { + d.cancel(); + if (AppConfig.DEBUG) + Log.d(TAG, "Cancelled all downloads"); + } + sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); + + } + queryDownloads(); + } + + }; + + private void onDownloadQueued(Intent intent) { + if (AppConfig.DEBUG) + Log.d(TAG, "Received enqueue request"); + DownloadRequest request = intent.getParcelableExtra(EXTRA_REQUEST); + if (request == null) { + throw new IllegalArgumentException( + "ACTION_ENQUEUE_DOWNLOAD intent needs request extra"); + } + + Downloader downloader = getDownloader(request); + if (downloader != null) { + numberOfDownloads.incrementAndGet(); + downloads.add(downloader); + downloadExecutor.submit(downloader); + sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); + } + + queryDownloads(); + } + + private Downloader getDownloader(DownloadRequest request) { + if (URLUtil.isHttpUrl(request.getSource())) { + return new HttpDownloader(request); + } + Log.e(TAG, + "Could not find appropriate downloader for " + + request.getSource()); + return null; + } + + /** + * Remove download from the DownloadRequester list and from the + * DownloadService list. + */ + private void removeDownload(final Downloader d) { + handler.post(new Runnable() { + @Override + public void run() { + if (AppConfig.DEBUG) + Log.d(TAG, "Removing downloader: " + + d.getDownloadRequest().getSource()); + boolean rc = downloads.remove(d); + if (AppConfig.DEBUG) + Log.d(TAG, "Result of downloads.remove: " + rc); + DownloadRequester.getInstance().removeDownload(d.getDownloadRequest()); + sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); + } + }); + } + + /** + * Adds a new DownloadStatus object to the list of completed downloads and + * saves it in the database + * + * @param status the download that is going to be saved + */ + private void saveDownloadStatus(DownloadStatus status) { + completedDownloads.add(status); + DBWriter.addDownloadStatus(this, status); + } + + private void sendDownloadHandledIntent() { + EventDistributor.getInstance().sendDownloadHandledBroadcast(); + } + + /** + * Creates a notification at the end of the service lifecycle to notify the + * user about the number of completed downloads. A report will only be + * created if the number of successfully downloaded feeds is bigger than 1 + * or if there is at least one failed download which is not an image or if + * there is at least one downloaded media file. + */ + private void updateReport() { + // check if report should be created + boolean createReport = false; + int successfulDownloads = 0; + int failedDownloads = 0; + + // a download report is created if at least one download has failed + // (excluding failed image downloads) + for (DownloadStatus status : completedDownloads) { + if (status.isSuccessful()) { + successfulDownloads++; + } else if (!status.isCancelled()) { + if (status.getFeedfileType() != FeedImage.FEEDFILETYPE_FEEDIMAGE) { + createReport = true; + } + failedDownloads++; + } + } + + if (createReport) { + if (AppConfig.DEBUG) + Log.d(TAG, "Creating report"); + // create notification object + Notification notification = new NotificationCompat.Builder(this) + .setTicker( + getString(de.danoeh.antennapod.R.string.download_report_title)) + .setContentTitle( + getString(de.danoeh.antennapod.R.string.download_report_title)) + .setContentText( + String.format( + getString(R.string.download_report_content), + successfulDownloads, failedDownloads)) + .setSmallIcon(R.drawable.stat_notify_sync) + .setLargeIcon( + BitmapFactory.decodeResource(getResources(), + R.drawable.stat_notify_sync)) + .setContentIntent( + PendingIntent.getActivity(this, 0, new Intent(this, + DownloadLogActivity.class), 0)) + .setAutoCancel(true).getNotification(); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(REPORT_ID, notification); + } else { + if (AppConfig.DEBUG) + Log.d(TAG, "No report is created"); + } + completedDownloads.clear(); + } + + /** + * Calls query downloads on the services main thread. This method should be used instead of queryDownloads if it is + * used from a thread other than the main thread. + */ + void queryDownloadsAsync() { + handler.post(new Runnable() { + public void run() { + queryDownloads(); + ; + } + }); + } + + /** + * Check if there's something else to download, otherwise stop + */ + void queryDownloads() { + if (AppConfig.DEBUG) { + Log.d(TAG, numberOfDownloads.get() + " downloads left"); + } + + if (numberOfDownloads.get() <= 0 && DownloadRequester.getInstance().hasNoDownloads()) { + if (AppConfig.DEBUG) + Log.d(TAG, "Number of downloads is " + numberOfDownloads.get() + ", attempting shutdown"); + stopSelf(); + } else { + setupNotificationUpdater(); + startForeground(NOTIFICATION_ID, updateNotifications()); + } + } + + /** + * Is called whenever a Feed is downloaded + */ + private void handleCompletedFeedDownload(DownloadRequest request) { + if (AppConfig.DEBUG) + Log.d(TAG, "Handling completed Feed Download"); + syncExecutor.execute(new FeedSyncThread(request)); + + } + + /** + * Is called whenever a Feed-Image is downloaded + */ + private void handleCompletedImageDownload(DownloadStatus status, DownloadRequest request) { + if (AppConfig.DEBUG) + Log.d(TAG, "Handling completed Image Download"); + syncExecutor.execute(new ImageHandlerThread(status, request)); + } + + /** + * Is called whenever a FeedMedia is downloaded. + */ + private void handleCompletedFeedMediaDownload(DownloadStatus status, DownloadRequest request) { + if (AppConfig.DEBUG) + Log.d(TAG, "Handling completed FeedMedia Download"); + syncExecutor.execute(new MediaHandlerThread(status, request)); + } + + /** + * Takes a single Feed, parses the corresponding file and refreshes + * information in the manager + */ + class FeedSyncThread implements Runnable { + private static final String TAG = "FeedSyncThread"; + + private DownloadRequest request; + + private DownloadError reason; + private boolean successful; + + public FeedSyncThread(DownloadRequest request) { + if (request == null) { + throw new IllegalArgumentException("Request must not be null"); + } + + this.request = request; + } + + public void run() { + Feed savedFeed = null; + + Feed feed = new Feed(request.getSource(), new Date()); + feed.setFile_url(request.getDestination()); + feed.setDownloaded(true); + + reason = null; + String reasonDetailed = null; + successful = true; + FeedHandler feedHandler = new FeedHandler(); + + try { + feed = feedHandler.parseFeed(feed); + if (AppConfig.DEBUG) + Log.d(TAG, feed.getTitle() + " parsed"); + if (checkFeedData(feed) == false) { + throw new InvalidFeedException(); + } + // Save information of feed in DB + savedFeed = DBTasks.updateFeed(DownloadService.this, feed); + // Download Feed Image if provided and not downloaded + if (savedFeed.getImage() != null + && savedFeed.getImage().isDownloaded() == false) { + if (AppConfig.DEBUG) + Log.d(TAG, "Feed has image; Downloading...."); + savedFeed.getImage().setFeed(savedFeed); + final Feed savedFeedRef = savedFeed; + try { + requester.downloadImage(DownloadService.this, + savedFeedRef.getImage()); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DBWriter.addDownloadStatus( + DownloadService.this, + new DownloadStatus( + savedFeedRef.getImage(), + savedFeedRef + .getImage() + .getHumanReadableIdentifier(), + DownloadError.ERROR_REQUEST_ERROR, + false, e.getMessage())); + } + } + + } catch (SAXException e) { + successful = false; + e.printStackTrace(); + reason = DownloadError.ERROR_PARSER_EXCEPTION; + reasonDetailed = e.getMessage(); + } catch (IOException e) { + successful = false; + e.printStackTrace(); + reason = DownloadError.ERROR_PARSER_EXCEPTION; + reasonDetailed = e.getMessage(); + } catch (ParserConfigurationException e) { + successful = false; + e.printStackTrace(); + reason = DownloadError.ERROR_PARSER_EXCEPTION; + reasonDetailed = e.getMessage(); + } catch (UnsupportedFeedtypeException e) { + e.printStackTrace(); + successful = false; + reason = DownloadError.ERROR_UNSUPPORTED_TYPE; + reasonDetailed = e.getMessage(); + } catch (InvalidFeedException e) { + e.printStackTrace(); + successful = false; + reason = DownloadError.ERROR_PARSER_EXCEPTION; + reasonDetailed = e.getMessage(); + } + + // cleanup(); + if (savedFeed == null) { + savedFeed = feed; + } + + saveDownloadStatus(new DownloadStatus(savedFeed, + savedFeed.getHumanReadableIdentifier(), reason, successful, + reasonDetailed)); + sendDownloadHandledIntent(); + numberOfDownloads.decrementAndGet(); + queryDownloadsAsync(); + } + + /** + * Checks if the feed was parsed correctly. + */ + private boolean checkFeedData(Feed feed) { + if (feed.getTitle() == null) { + Log.e(TAG, "Feed has no title."); + return false; + } + if (!hasValidFeedItems(feed)) { + Log.e(TAG, "Feed has invalid items"); + return false; + } + if (AppConfig.DEBUG) + Log.d(TAG, "Feed appears to be valid."); + return true; + + } + + private boolean hasValidFeedItems(Feed feed) { + for (FeedItem item : feed.getItems()) { + if (item.getTitle() == null) { + Log.e(TAG, "Item has no title"); + return false; + } + if (item.getPubDate() == null) { + Log.e(TAG, + "Item has no pubDate. Using current time as pubDate"); + if (item.getTitle() != null) { + Log.e(TAG, "Title of invalid item: " + item.getTitle()); + } + item.setPubDate(new Date()); + } + } + return true; + } + + /** + * Delete files that aren't needed anymore + */ + private void cleanup(Feed feed) { + if (feed.getFile_url() != null) { + if (new File(feed.getFile_url()).delete()) + if (AppConfig.DEBUG) + Log.d(TAG, "Successfully deleted cache file."); + else + Log.e(TAG, "Failed to delete cache file."); + feed.setFile_url(null); + } else if (AppConfig.DEBUG) { + Log.d(TAG, "Didn't delete cache file: File url is not set."); + } + } + + } + + /** + * Handles a completed image download. + */ + class ImageHandlerThread implements Runnable { + + private DownloadRequest request; + private DownloadStatus status; + + public ImageHandlerThread(DownloadStatus status, DownloadRequest request) { + if (status == null) { + throw new IllegalArgumentException("Status must not be null"); + } + if (request == null) { + throw new IllegalArgumentException("Request must not be null"); + } + this.status = status; + this.request = request; + } + + @Override + public void run() { + FeedImage image = DBReader.getFeedImage(DownloadService.this, request.getFeedfileId()); + if (image == null) { + throw new IllegalStateException("Could not find downloaded image in database"); + } + + image.setFile_url(request.getDestination()); + image.setDownloaded(true); + + saveDownloadStatus(status); + sendDownloadHandledIntent(); + DBWriter.setFeedImage(DownloadService.this, image); + numberOfDownloads.decrementAndGet(); + queryDownloadsAsync(); + } + } + + /** + * Handles a completed media download. + */ + class MediaHandlerThread implements Runnable { + + private DownloadRequest request; + private DownloadStatus status; + + public MediaHandlerThread(DownloadStatus status, DownloadRequest request) { + if (status == null) { + throw new IllegalArgumentException("Status must not be null"); + } + if (request == null) { + throw new IllegalArgumentException("Request must not be null"); + } + + this.status = status; + this.request = request; + } + + @Override + public void run() { + FeedMedia media = DBReader.getFeedMedia(DownloadService.this, + request.getFeedfileId()); + if (media == null) { + throw new IllegalStateException( + "Could not find downloaded media object in database"); + } + boolean chaptersRead = false; + media.setDownloaded(true); + media.setFile_url(request.getDestination()); + + // Get duration + MediaPlayer mediaplayer = null; + try { + mediaplayer = new MediaPlayer(); + mediaplayer.setDataSource(media.getFile_url()); + mediaplayer.prepare(); + media.setDuration(mediaplayer.getDuration()); + if (AppConfig.DEBUG) + Log.d(TAG, "Duration of file is " + media.getDuration()); + mediaplayer.reset(); + } catch (IOException e) { + e.printStackTrace(); + } catch (RuntimeException e) { + // Thrown by MediaPlayer initialization on some devices + e.printStackTrace(); + } finally { + if (mediaplayer != null) { + mediaplayer.release(); + } + } + + if (media.getItem().getChapters() == null) { + ChapterUtils.loadChaptersFromFileUrl(media); + if (media.getItem().getChapters() != null) { + chaptersRead = true; + } + } + + saveDownloadStatus(status); + sendDownloadHandledIntent(); + + try { + if (chaptersRead) { + DBWriter.setFeedItem(DownloadService.this, media.getItem()).get(); + } + DBWriter.setFeedMedia(DownloadService.this, media).get(); + } catch (ExecutionException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + if (!DBTasks.isInQueue(DownloadService.this, media.getItem().getId())) { + DBWriter.addQueueItem(DownloadService.this, media.getItem().getId()); + } + + numberOfDownloads.decrementAndGet(); + queryDownloadsAsync(); + } + } + + /** + * Schedules the notification updater task if it hasn't been scheduled yet. + */ + private void setupNotificationUpdater() { + if (AppConfig.DEBUG) + Log.d(TAG, "Setting up notification updater"); + if (notificationUpdater == null) { + notificationUpdater = new NotificationUpdater(); + notificationUpdaterFuture = schedExecutor.scheduleAtFixedRate( + notificationUpdater, 5L, 5L, TimeUnit.SECONDS); + } + } + + private void cancelNotificationUpdater() { + boolean result = false; + if (notificationUpdaterFuture != null) { + result = notificationUpdaterFuture.cancel(true); + } + notificationUpdater = null; + notificationUpdaterFuture = null; + Log.d(TAG, "NotificationUpdater cancelled. Result: " + result); + } + + private class NotificationUpdater implements Runnable { + public void run() { + handler.post(new Runnable() { + @Override + public void run() { + Notification n = updateNotifications(); + if (n != null) { + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(NOTIFICATION_ID, n); + } + } + }); + } + } + + public List<Downloader> getDownloads() { + return downloads; + } } diff --git a/src/de/danoeh/antennapod/service/download/DownloadStatus.java b/src/de/danoeh/antennapod/service/download/DownloadStatus.java new file mode 100644 index 000000000..487c3b3de --- /dev/null +++ b/src/de/danoeh/antennapod/service/download/DownloadStatus.java @@ -0,0 +1,182 @@ +package de.danoeh.antennapod.service.download; + +import java.util.Date; + +import de.danoeh.antennapod.feed.FeedFile; +import de.danoeh.antennapod.util.DownloadError; + +/** Contains status attributes for one download */ +public class DownloadStatus { + /** + * Downloaders should use this constant for the size attribute if necessary + * so that the listadapters etc. can react properly. + */ + public static final int SIZE_UNKNOWN = -1; + + // ----------------------------------- ATTRIBUTES STORED IN DB + /** Unique id for storing the object in database. */ + protected long id; + /** + * A human-readable string which is shown to the user so that he can + * identify the download. Should be the title of the item/feed/media or the + * URL if the download has no other title. + */ + protected String title; + protected DownloadError reason; + /** + * A message which can be presented to the user to give more information. + * Should be null if Download was successful. + */ + protected String reasonDetailed; + protected boolean successful; + protected Date completionDate; + protected long feedfileId; + /** + * Is used to determine the type of the feedfile even if the feedfile does + * not exist anymore. The value should be FEEDFILETYPE_FEED, + * FEEDFILETYPE_FEEDIMAGE or FEEDFILETYPE_FEEDMEDIA + */ + protected int feedfileType; + + // ------------------------------------ NOT STORED IN DB + protected boolean done; + protected boolean cancelled; + + /** Constructor for restoring Download status entries from DB. */ + public DownloadStatus(long id, String title, long feedfileId, + int feedfileType, boolean successful, DownloadError reason, + Date completionDate, String reasonDetailed) { + this.id = id; + this.title = title; + this.done = true; + this.feedfileId = feedfileId; + this.reason = reason; + this.successful = successful; + this.completionDate = (Date) completionDate.clone(); + this.reasonDetailed = reasonDetailed; + this.feedfileType = feedfileType; + } + + public DownloadStatus(DownloadRequest request, DownloadError reason, + boolean successful, boolean cancelled, String reasonDetailed) { + if (request == null) { + throw new IllegalArgumentException("request must not be null"); + } + this.title = request.getTitle(); + this.feedfileId = request.getFeedfileId(); + this.feedfileType = request.getFeedfileType(); + this.reason = reason; + this.successful = successful; + this.cancelled = cancelled; + this.reasonDetailed = reasonDetailed; + this.completionDate = new Date(); + } + + /** Constructor for creating new completed downloads. */ + public DownloadStatus(FeedFile feedfile, String title, DownloadError reason, + boolean successful, String reasonDetailed) { + if (feedfile == null) { + throw new IllegalArgumentException("feedfile must not be null"); + } + + this.title = title; + this.done = true; + this.feedfileId = feedfile.getId(); + this.feedfileType = feedfile.getTypeAsInt(); + this.reason = reason; + this.successful = successful; + this.completionDate = new Date(); + this.reasonDetailed = reasonDetailed; + } + + /** Constructor for creating new completed downloads. */ + public DownloadStatus(long feedfileId, int feedfileType, String title, + DownloadError reason, boolean successful, String reasonDetailed) { + this.title = title; + this.done = true; + this.feedfileId = feedfileId; + this.feedfileType = feedfileType; + this.reason = reason; + this.successful = successful; + this.completionDate = new Date(); + this.reasonDetailed = reasonDetailed; + } + + @Override + public String toString() { + return "DownloadStatus [id=" + id + ", title=" + title + ", reason=" + + reason + ", reasonDetailed=" + reasonDetailed + + ", successful=" + successful + ", completionDate=" + + completionDate + ", feedfileId=" + feedfileId + + ", feedfileType=" + feedfileType + ", done=" + done + + ", cancelled=" + cancelled + "]"; + } + + public long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public DownloadError getReason() { + return reason; + } + + public String getReasonDetailed() { + return reasonDetailed; + } + + public boolean isSuccessful() { + return successful; + } + + public Date getCompletionDate() { + return (Date) completionDate.clone(); + } + + public long getFeedfileId() { + return feedfileId; + } + + public int getFeedfileType() { + return feedfileType; + } + + public boolean isDone() { + return done; + } + + public boolean isCancelled() { + return cancelled; + } + + public void setSuccessful() { + this.successful = true; + this.reason = DownloadError.SUCCESS; + this.done = true; + } + + public void setFailed(DownloadError reason, String reasonDetailed) { + this.successful = false; + this.reason = reason; + this.reasonDetailed = reasonDetailed; + this.done = true; + } + + public void setCancelled() { + this.successful = false; + this.reason = DownloadError.ERROR_DOWNLOAD_CANCELLED; + this.done = true; + this.cancelled = true; + } + + public void setCompletionDate(Date completionDate) { + this.completionDate = (Date) completionDate.clone(); + } + + public void setId(long id) { + this.id = id; + } +}
\ No newline at end of file diff --git a/src/de/danoeh/antennapod/service/download/Downloader.java b/src/de/danoeh/antennapod/service/download/Downloader.java index 9ed9d9a76..84731fe9f 100644 --- a/src/de/danoeh/antennapod/service/download/Downloader.java +++ b/src/de/danoeh/antennapod/service/download/Downloader.java @@ -1,49 +1,50 @@ package de.danoeh.antennapod.service.download; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.asynctask.DownloadStatus; + +import java.util.concurrent.Callable; /** Downloads files */ -public abstract class Downloader extends Thread { +public abstract class Downloader implements Callable<Downloader> { private static final String TAG = "Downloader"; - private DownloaderCallback downloaderCallback; - protected boolean finished; + protected volatile boolean finished; protected volatile boolean cancelled; - protected volatile DownloadStatus status; + protected DownloadRequest request; + protected DownloadStatus result; - public Downloader(DownloaderCallback downloaderCallback, - DownloadStatus status) { + public Downloader(DownloadRequest request) { super(); - this.downloaderCallback = downloaderCallback; - this.status = status; - this.status.setStatusMsg(R.string.download_pending); + this.request = request; + this.request.setStatusMsg(R.string.download_pending); this.cancelled = false; + this.result = new DownloadStatus(request, null, false, false, null); } - /** - * This method must be called when the download was completed, failed, or - * was cancelled - */ - protected void finish() { - if (!finished) { - finished = true; - downloaderCallback.onDownloadCompleted(this); + protected abstract void download(); + + public final Downloader call() { + download(); + if (result == null) { + throw new IllegalStateException( + "Downloader hasn't created DownloadStatus object"); } + finished = true; + return this; } - protected abstract void download(); + public DownloadRequest getDownloadRequest() { + return request; + } - @Override - public final void run() { - download(); - finish(); + public DownloadStatus getResult() { + return result; } - public DownloadStatus getStatus() { - return status; + public boolean isFinished() { + return finished; } public void cancel() { diff --git a/src/de/danoeh/antennapod/service/download/HttpDownloader.java b/src/de/danoeh/antennapod/service/download/HttpDownloader.java index f8f26f6fd..582fb9575 100644 --- a/src/de/danoeh/antennapod/service/download/HttpDownloader.java +++ b/src/de/danoeh/antennapod/service/download/HttpDownloader.java @@ -6,12 +6,12 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import org.apache.commons.io.IOUtils; +import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; @@ -26,178 +26,190 @@ import android.util.Log; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.PodcastApp; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.asynctask.DownloadStatus; import de.danoeh.antennapod.util.DownloadError; import de.danoeh.antennapod.util.StorageUtils; public class HttpDownloader extends Downloader { - private static final String TAG = "HttpDownloader"; - - private static final int MAX_REDIRECTS = 5; - - private static final int BUFFER_SIZE = 8 * 1024; - private static final int CONNECTION_TIMEOUT = 30000; - private static final int SOCKET_TIMEOUT = 30000; - - public HttpDownloader(DownloaderCallback downloaderCallback, - DownloadStatus status) { - super(downloaderCallback, status); - } - - private DefaultHttpClient createHttpClient() { - DefaultHttpClient httpClient = new DefaultHttpClient(); - HttpParams params = httpClient.getParams(); - params.setIntParameter("http.protocol.max-redirects", MAX_REDIRECTS); - params.setBooleanParameter("http.protocol.reject-relative-redirect", - false); - HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT); - HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT); - HttpClientParams.setRedirecting(params, true); - - // Workaround for broken URLs in redirection - ((AbstractHttpClient) httpClient) - .setRedirectHandler(new APRedirectHandler()); - return httpClient; - } - - @Override - protected void download() { - DefaultHttpClient httpClient = null; - OutputStream out = null; - InputStream connection = null; - try { - HttpGet httpGet = new HttpGet(status.getFeedFile() - .getDownload_url()); - httpClient = createHttpClient(); - HttpResponse response = httpClient.execute(httpGet); - HttpEntity httpEntity = response.getEntity(); - int responseCode = response.getStatusLine().getStatusCode(); - if (AppConfig.DEBUG) - Log.d(TAG, "Response code is " + responseCode); - if (responseCode == HttpURLConnection.HTTP_OK && httpEntity != null) { - if (StorageUtils.storageAvailable(PodcastApp.getInstance())) { - File destination = new File(status.getFeedFile() - .getFile_url()); - if (!destination.exists()) { - connection = AndroidHttpClient - .getUngzippedContent(httpEntity); - InputStream in = new BufferedInputStream(connection); - out = new BufferedOutputStream(new FileOutputStream( - destination)); - byte[] buffer = new byte[BUFFER_SIZE]; - int count = 0; - status.setStatusMsg(R.string.download_running); - if (AppConfig.DEBUG) - Log.d(TAG, "Getting size of download"); - status.setSize(httpEntity.getContentLength()); - if (AppConfig.DEBUG) - Log.d(TAG, "Size is " + status.getSize()); - if (status.getSize() < 0) { - status.setSize(DownloadStatus.SIZE_UNKNOWN); - } - - long freeSpace = StorageUtils.getFreeSpaceAvailable(); - if (AppConfig.DEBUG) - Log.d(TAG, "Free space is " + freeSpace); - if (status.getSize() == DownloadStatus.SIZE_UNKNOWN - || status.getSize() <= freeSpace) { - if (AppConfig.DEBUG) - Log.d(TAG, "Starting download"); - while (!cancelled - && (count = in.read(buffer)) != -1) { - out.write(buffer, 0, count); - status.setSoFar(status.getSoFar() + count); - status.setProgressPercent((int) (((double) status - .getSoFar() / (double) status.getSize()) * 100)); - } - if (cancelled) { - onCancelled(); - } else { - onSuccess(); - } - } else { - onFail(DownloadError.ERROR_NOT_ENOUGH_SPACE, null); - } - } else { - Log.w(TAG, "File already exists"); - onFail(DownloadError.ERROR_FILE_EXISTS, null); - } - } else { - onFail(DownloadError.ERROR_DEVICE_NOT_FOUND, null); - } - } else { - onFail(DownloadError.ERROR_HTTP_DATA_ERROR, - String.valueOf(responseCode)); - } - } catch (IllegalArgumentException e) { - e.printStackTrace(); - onFail(DownloadError.ERROR_MALFORMED_URL, e.getMessage()); - } catch (SocketTimeoutException e) { - e.printStackTrace(); - onFail(DownloadError.ERROR_CONNECTION_ERROR, e.getMessage()); - } catch (UnknownHostException e) { - e.printStackTrace(); - onFail(DownloadError.ERROR_UNKNOWN_HOST, e.getMessage()); - } catch (IOException e) { - e.printStackTrace(); - onFail(DownloadError.ERROR_IO_ERROR, e.getMessage()); - } catch (NullPointerException e) { - // might be thrown by connection.getInputStream() - e.printStackTrace(); - onFail(DownloadError.ERROR_CONNECTION_ERROR, status.getFeedFile() - .getDownload_url()); - } finally { - IOUtils.closeQuietly(connection); - IOUtils.closeQuietly(out); - if (httpClient != null) { - httpClient.getConnectionManager().shutdown(); - } - } - } - - private void onSuccess() { - if (AppConfig.DEBUG) - Log.d(TAG, "Download was successful"); - status.setSuccessful(true); - status.setDone(true); - } - - private void onFail(int reason, String reasonDetailed) { - if (AppConfig.DEBUG) { - Log.d(TAG, "Download failed"); - } - status.setReason(reason); - status.setReasonDetailed(reasonDetailed); - status.setDone(true); - status.setSuccessful(false); - cleanup(); - } - - private void onCancelled() { - if (AppConfig.DEBUG) - Log.d(TAG, "Download was cancelled"); - status.setReason(DownloadError.ERROR_DOWNLOAD_CANCELLED); - status.setDone(true); - status.setSuccessful(false); - status.setCancelled(true); - cleanup(); - } - - /** Deletes unfinished downloads. */ - private void cleanup() { - if (status != null && status.getFeedFile() != null - && status.getFeedFile().getFile_url() != null) { - File dest = new File(status.getFeedFile().getFile_url()); - if (dest.exists()) { - boolean rc = dest.delete(); - if (AppConfig.DEBUG) - Log.d(TAG, "Deleted file " + dest.getName() + "; Result: " - + rc); - } else { - if (AppConfig.DEBUG) - Log.d(TAG, "cleanup() didn't delete file: does not exist."); - } - } - } + private static final String TAG = "HttpDownloader"; + + private static final int MAX_REDIRECTS = 5; + + private static final int BUFFER_SIZE = 8 * 1024; + private static final int CONNECTION_TIMEOUT = 30000; + private static final int SOCKET_TIMEOUT = 30000; + + public HttpDownloader(DownloadRequest request) { + super(request); + } + + private DefaultHttpClient createHttpClient() { + DefaultHttpClient httpClient = new DefaultHttpClient(); + HttpParams params = httpClient.getParams(); + params.setIntParameter("http.protocol.max-redirects", MAX_REDIRECTS); + params.setBooleanParameter("http.protocol.reject-relative-redirect", + false); + HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT); + HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT); + HttpClientParams.setRedirecting(params, true); + + // Workaround for broken URLs in redirection + ((AbstractHttpClient) httpClient) + .setRedirectHandler(new APRedirectHandler()); + return httpClient; + } + + @Override + protected void download() { + DefaultHttpClient httpClient = null; + BufferedOutputStream out = null; + InputStream connection = null; + try { + HttpGet httpGet = new HttpGet(request.getSource()); + httpClient = createHttpClient(); + HttpResponse response = httpClient.execute(httpGet); + HttpEntity httpEntity = response.getEntity(); + int responseCode = response.getStatusLine().getStatusCode(); + Header contentEncodingHeader = response.getFirstHeader("Content-Encoding"); + + final boolean isGzip = contentEncodingHeader != null && + contentEncodingHeader.getValue().equalsIgnoreCase("gzip"); + + if (AppConfig.DEBUG) + Log.d(TAG, "Response code is " + responseCode); + + if (responseCode != HttpURLConnection.HTTP_OK || httpEntity == null) { + onFail(DownloadError.ERROR_HTTP_DATA_ERROR, + String.valueOf(responseCode)); + return; + } + + if (!StorageUtils.storageAvailable(PodcastApp.getInstance())) { + onFail(DownloadError.ERROR_DEVICE_NOT_FOUND, null); + return; + } + + File destination = new File(request.getDestination()); + if (destination.exists()) { + Log.w(TAG, "File already exists"); + onFail(DownloadError.ERROR_FILE_EXISTS, null); + return; + } + + connection = new BufferedInputStream(AndroidHttpClient + .getUngzippedContent(httpEntity)); + out = new BufferedOutputStream(new FileOutputStream( + destination)); + byte[] buffer = new byte[BUFFER_SIZE]; + int count = 0; + request.setStatusMsg(R.string.download_running); + if (AppConfig.DEBUG) + Log.d(TAG, "Getting size of download"); + request.setSize(httpEntity.getContentLength()); + if (AppConfig.DEBUG) + Log.d(TAG, "Size is " + request.getSize()); + if (request.getSize() < 0) { + request.setSize(DownloadStatus.SIZE_UNKNOWN); + } + + long freeSpace = StorageUtils.getFreeSpaceAvailable(); + if (AppConfig.DEBUG) + Log.d(TAG, "Free space is " + freeSpace); + + if (request.getSize() != DownloadStatus.SIZE_UNKNOWN + && request.getSize() > freeSpace) { + onFail(DownloadError.ERROR_NOT_ENOUGH_SPACE, null); + return; + } + + if (AppConfig.DEBUG) + Log.d(TAG, "Starting download"); + while (!cancelled + && (count = connection.read(buffer)) != -1) { + out.write(buffer, 0, count); + request.setSoFar(request.getSoFar() + count); + request.setProgressPercent((int) (((double) request + .getSoFar() / (double) request + .getSize()) * 100)); + } + if (cancelled) { + onCancelled(); + } else { + out.flush(); + // check if size specified in the response header is the same as the size of the + // written file. This check cannot be made if compression was used + if (!isGzip && request.getSize() != DownloadStatus.SIZE_UNKNOWN && + request.getSoFar() != request.getSize()) { + onFail(DownloadError.ERROR_IO_ERROR, + "Download completed but size: " + + request.getSoFar() + + " does not equal expected size " + + request.getSize()); + return; + } + onSuccess(); + } + + } catch (IllegalArgumentException e) { + e.printStackTrace(); + onFail(DownloadError.ERROR_MALFORMED_URL, e.getMessage()); + } catch (SocketTimeoutException e) { + e.printStackTrace(); + onFail(DownloadError.ERROR_CONNECTION_ERROR, e.getMessage()); + } catch (UnknownHostException e) { + e.printStackTrace(); + onFail(DownloadError.ERROR_UNKNOWN_HOST, e.getMessage()); + } catch (IOException e) { + e.printStackTrace(); + onFail(DownloadError.ERROR_IO_ERROR, e.getMessage()); + } catch (NullPointerException e) { + // might be thrown by connection.getInputStream() + e.printStackTrace(); + onFail(DownloadError.ERROR_CONNECTION_ERROR, request.getSource()); + } finally { + IOUtils.closeQuietly(out); + if (httpClient != null) { + httpClient.getConnectionManager().shutdown(); + } + } + } + + private void onSuccess() { + if (AppConfig.DEBUG) + Log.d(TAG, "Download was successful"); + result.setSuccessful(); + } + + private void onFail(DownloadError reason, String reasonDetailed) { + if (AppConfig.DEBUG) { + Log.d(TAG, "Download failed"); + } + result.setFailed(reason, reasonDetailed); + cleanup(); + } + + private void onCancelled() { + if (AppConfig.DEBUG) + Log.d(TAG, "Download was cancelled"); + result.setCancelled(); + cleanup(); + } + + /** + * Deletes unfinished downloads. + */ + private void cleanup() { + if (request.getDestination() != null) { + File dest = new File(request.getDestination()); + if (dest.exists()) { + boolean rc = dest.delete(); + if (AppConfig.DEBUG) + Log.d(TAG, "Deleted file " + dest.getName() + "; Result: " + + rc); + } else { + if (AppConfig.DEBUG) + Log.d(TAG, "cleanup() didn't delete file: does not exist."); + } + } + } } |