Merge branch 'downloadmanager' into develop

This commit is contained in:
daniel oeh 2012-08-18 00:53:09 +02:00
commit fdb9a296ad
23 changed files with 890 additions and 522 deletions

View File

@ -11,8 +11,6 @@
<uses-sdk
android:minSdkVersion="10"
android:targetSdkVersion="14" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<supports-screens
@ -74,7 +72,7 @@
android:theme="@style/Theme.MediaPlayer" android:screenOrientation="portrait"/>
<service
android:name="de.danoeh.antennapod.service.DownloadService"
android:name=".service.download.DownloadService"
android:enabled="true" />
<service
android:name="de.danoeh.antennapod.service.PlaybackService"
@ -200,4 +198,4 @@
<activity android:name=".activity.VideoplayerActivity" android:configChanges="keyboardHidden|orientation" android:screenOrientation="landscape" android:theme="@style/VideoplayerTheme"></activity>
</application>
</manifest>
</manifest>

View File

@ -87,7 +87,7 @@
<string name="pref_pauseOnHeadsetDisconnect_title">Headphones disconnect</string>
<string name="pref_mobileUpdate_title">Mobile updates</string>
<string name="pref_mobileUpdate_sum">Allow updates over the mobile data connection</string>
<string name="download_report_title">All downloads completed</string>
<string name="download_report_title">Downloads completed</string>
<string name="refresh_label">Refresh</string>
<string name="external_storage_error_msg">No external storage is available. Please make sure that external storage is mounted so that the app can work properly.</string>
<string name="share_link_label">Share link</string>
@ -176,6 +176,9 @@
<string name="user_interface_label">User Interface</string>
<string name="feed_delete_confirmation_msg">Please confirm that you want to delete this feed and ALL episodes of this feed that you have downloaded.</string>
<string name="image_of_prefix">Image of:\u0020</string>
<string name="download_error_malformed_url">Malformed URL</string>
<string name="download_error_io_error">IO Error</string>
<string name="download_error_device_not_found">External storage unavailable</string>
</resources>

View File

@ -23,7 +23,7 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.asynctask.DownloadStatus;
import de.danoeh.antennapod.feed.Feed;
import de.danoeh.antennapod.feed.FeedManager;
import de.danoeh.antennapod.service.DownloadService;
import de.danoeh.antennapod.service.download.DownloadService;
import de.danoeh.antennapod.storage.DownloadRequester;
import de.danoeh.antennapod.util.ConnectionTester;
import de.danoeh.antennapod.util.DownloadError;

View File

@ -1,8 +1,15 @@
package de.danoeh.antennapod.activity;
import java.util.List;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
@ -18,9 +25,9 @@ import com.actionbarsherlock.view.MenuItem;
import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.adapter.DownloadlistAdapter;
import de.danoeh.antennapod.asynctask.DownloadObserver;
import de.danoeh.antennapod.asynctask.DownloadStatus;
import de.danoeh.antennapod.service.DownloadService;
import de.danoeh.antennapod.service.download.DownloadService;
import de.danoeh.antennapod.service.download.Downloader;
import de.danoeh.antennapod.storage.DownloadRequester;
/**
@ -28,7 +35,7 @@ import de.danoeh.antennapod.storage.DownloadRequester;
* objects created by a DownloadObserver.
*/
public class DownloadActivity extends SherlockListActivity implements
ActionMode.Callback, DownloadObserver.Callback {
ActionMode.Callback {
private static final String TAG = "DownloadActivity";
private static final int MENU_SHOW_LOG = 0;
@ -38,7 +45,11 @@ public class DownloadActivity extends SherlockListActivity implements
private ActionMode mActionMode;
private DownloadStatus selectedDownload;
private DownloadObserver downloadObserver;
private DownloadService downloadService = null;
boolean mIsBound;
private AsyncTask<Void, Void, Void> contentRefresher;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -47,24 +58,25 @@ public class DownloadActivity extends SherlockListActivity implements
Log.d(TAG, "Creating Activity");
requester = DownloadRequester.getInstance();
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
protected void onPause() {
super.onPause();
unbindService(mConnection);
if (downloadObserver != null) {
downloadObserver.unregisterCallback(DownloadActivity.this);
}
unregisterReceiver(contentChanged);
}
@Override
protected void onResume() {
super.onResume();
if (AppConfig.DEBUG)
Log.d(TAG, "Trying to bind service");
registerReceiver(contentChanged, new IntentFilter(
DownloadService.ACTION_DOWNLOADS_CONTENT_CHANGED));
bindService(new Intent(this, DownloadService.class), mConnection, 0);
startContentRefresher();
if (dla != null) {
dla.notifyDataSetChanged();
}
}
@Override
@ -72,6 +84,71 @@ public class DownloadActivity extends SherlockListActivity implements
super.onStop();
if (AppConfig.DEBUG)
Log.d(TAG, "Stopping Activity");
stopContentRefresher();
}
private ServiceConnection mConnection = new ServiceConnection() {
public void onServiceDisconnected(ComponentName className) {
downloadService = null;
mIsBound = false;
Log.i(TAG, "Closed connection with DownloadService.");
}
public void onServiceConnected(ComponentName name, IBinder service) {
downloadService = ((DownloadService.LocalBinder) service)
.getService();
mIsBound = true;
if (AppConfig.DEBUG)
Log.d(TAG, "Connection to service established");
dla = new DownloadlistAdapter(DownloadActivity.this, 0,
downloadService.getDownloads());
setListAdapter(dla);
dla.notifyDataSetChanged();
}
};
@SuppressLint("NewApi")
private void startContentRefresher() {
if (contentRefresher != null) {
contentRefresher.cancel(true);
}
contentRefresher = new AsyncTask<Void, Void, Void>() {
private final int WAITING_INTERVALL = 1000;
@Override
protected void onProgressUpdate(Void... values) {
super.onProgressUpdate(values);
if (dla != null) {
if (AppConfig.DEBUG)
Log.d(TAG, "Refreshing content automatically");
dla.notifyDataSetChanged();
}
}
@Override
protected Void doInBackground(Void... params) {
while (!isCancelled()) {
try {
Thread.sleep(WAITING_INTERVALL);
publishProgress();
} catch (InterruptedException e) {
return null;
}
}
return null;
}
};
if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
contentRefresher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else {
contentRefresher.execute();
}
}
private void stopContentRefresher() {
if (contentRefresher != null) {
contentRefresher.cancel(true);
}
}
@Override
@ -82,7 +159,7 @@ public class DownloadActivity extends SherlockListActivity implements
@Override
public boolean onItemLongClick(AdapterView<?> arg0, View view,
int position, long id) {
DownloadStatus selection = dla.getItem(position);
DownloadStatus selection = dla.getItem(position).getStatus();
if (selection != null && mActionMode != null) {
mActionMode.finish();
}
@ -142,8 +219,7 @@ public class DownloadActivity extends SherlockListActivity implements
boolean handled = false;
switch (item.getItemId()) {
case R.id.cancel_download_item:
requester.cancelDownload(this, selectedDownload.getFeedFile()
.getDownloadId());
requester.cancelDownload(this, selectedDownload.getFeedFile());
handled = true;
break;
}
@ -158,53 +234,16 @@ public class DownloadActivity extends SherlockListActivity implements
dla.setSelectedItemIndex(DownloadlistAdapter.SELECTION_NONE);
}
private DownloadService downloadService = null;
boolean mIsBound;
private BroadcastReceiver contentChanged = new BroadcastReceiver() {
private ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
downloadService = ((DownloadService.LocalBinder) service)
.getService();
if (AppConfig.DEBUG)
Log.d(TAG, "Connection to service established");
dla = new DownloadlistAdapter(DownloadActivity.this, 0,
downloadService.getDownloadObserver().getStatusList());
setListAdapter(dla);
downloadObserver = downloadService.getDownloadObserver();
downloadObserver.registerCallback(DownloadActivity.this);
}
public void onServiceDisconnected(ComponentName className) {
downloadService = null;
mIsBound = false;
Log.i(TAG, "Closed connection with DownloadService.");
@Override
public void onReceive(Context context, Intent intent) {
if (dla != null) {
if (AppConfig.DEBUG)
Log.d(TAG, "Refreshing content");
dla.notifyDataSetChanged();
}
}
};
@Override
public void onProgressUpdate() {
runOnUiThread(new Runnable() {
@Override
public void run() {
dla.notifyDataSetChanged();
}
});
}
@Override
public void onFinish() {
if (AppConfig.DEBUG)
Log.d(TAG, "Observer has finished, clearing adapter");
runOnUiThread(new Runnable() {
@Override
public void run() {
dla.clear();
dla.notifyDataSetInvalidated();
}
});
}
}

View File

@ -1,5 +1,9 @@
package de.danoeh.antennapod.activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import com.actionbarsherlock.app.SherlockListActivity;
@ -9,7 +13,10 @@ import com.actionbarsherlock.view.MenuItem;
import de.danoeh.antennapod.adapter.DownloadLogAdapter;
import de.danoeh.antennapod.feed.FeedManager;
/** Displays completed and failed downloads in a list. The data comes from the FeedManager. */
/**
* Displays completed and failed downloads in a list. The data comes from the
* FeedManager.
*/
public class DownloadLogActivity extends SherlockListActivity {
private static final String TAG = "DownloadLogActivity";
@ -26,6 +33,20 @@ public class DownloadLogActivity extends SherlockListActivity {
setListAdapter(dla);
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(contentUpdate);
}
@Override
protected void onResume() {
super.onResume();
registerReceiver(contentUpdate, new IntentFilter(
FeedManager.ACTION_DOWNLOADLOG_UPDATE));
dla.notifyDataSetChanged();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
return true;
@ -43,4 +64,15 @@ public class DownloadLogActivity extends SherlockListActivity {
return true;
}
private BroadcastReceiver contentUpdate = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction()
.equals(FeedManager.ACTION_DOWNLOADLOG_UPDATE)) {
dla.notifyDataSetChanged();
}
}
};
}

View File

@ -23,8 +23,8 @@ import de.danoeh.antennapod.feed.FeedManager;
import de.danoeh.antennapod.fragment.FeedlistFragment;
import de.danoeh.antennapod.fragment.QueueFragment;
import de.danoeh.antennapod.fragment.UnreadItemlistFragment;
import de.danoeh.antennapod.service.DownloadService;
import de.danoeh.antennapod.service.PlaybackService;
import de.danoeh.antennapod.service.download.DownloadService;
import de.danoeh.antennapod.storage.DownloadRequester;
import de.danoeh.antennapod.util.StorageUtils;
import de.danoeh.antennapod.AppConfig;

View File

@ -14,16 +14,17 @@ import de.danoeh.antennapod.feed.Feed;
import de.danoeh.antennapod.feed.FeedFile;
import de.danoeh.antennapod.feed.FeedImage;
import de.danoeh.antennapod.feed.FeedMedia;
import de.danoeh.antennapod.service.download.Downloader;
import de.danoeh.antennapod.util.Converter;
import de.danoeh.antennapod.R;
public class DownloadlistAdapter extends ArrayAdapter<DownloadStatus> {
public class DownloadlistAdapter extends ArrayAdapter<Downloader> {
private int selectedItemIndex;
public static final int SELECTION_NONE = -1;
public DownloadlistAdapter(Context context, int textViewResourceId,
List<DownloadStatus> objects) {
List<Downloader> objects) {
super(context, textViewResourceId, objects);
this.selectedItemIndex = SELECTION_NONE;
}
@ -31,7 +32,7 @@ public class DownloadlistAdapter extends ArrayAdapter<DownloadStatus> {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Holder holder;
DownloadStatus status = getItem(position);
DownloadStatus status = getItem(position).getStatus();
FeedFile feedFile = status.getFeedFile();
// Inflate layout
if (convertView == null) {
@ -77,7 +78,9 @@ public class DownloadlistAdapter extends ArrayAdapter<DownloadStatus> {
}
}
holder.title.setText(titleText);
holder.message.setText(status.getStatusMsg());
if (status.getStatusMsg() != 0) {
holder.message.setText(status.getStatusMsg());
}
holder.downloaded.setText(Converter.byteToString(status.getSoFar())
+ " / " + Converter.byteToString(status.getSize()));
holder.percent.setText(status.getProgressPercent() + "%");

View File

@ -5,6 +5,7 @@ import java.util.List;
import de.danoeh.antennapod.feed.FeedItem;
import de.danoeh.antennapod.feed.FeedManager;
import de.danoeh.antennapod.storage.DownloadRequester;
import de.danoeh.antennapod.util.Converter;
import de.danoeh.antennapod.R;
import android.widget.ArrayAdapter;
@ -121,7 +122,8 @@ public class FeedItemlistAdapter extends ArrayAdapter<FeedItem> {
holder.downloaded.setVisibility(View.GONE);
}
if (item.getMedia().isDownloading()) {
if (DownloadRequester.getInstance().isDownloadingFile(
item.getMedia())) {
holder.downloading.setVisibility(View.VISIBLE);
} else {
holder.downloading.setVisibility(View.GONE);

View File

@ -1,4 +1,4 @@
package de.danoeh.antennapod.asynctask;
/*package de.danoeh.antennapod.asynctask;
import java.util.ArrayList;
import java.util.Collections;
@ -16,16 +16,17 @@ import de.danoeh.antennapod.storage.DownloadRequester;
import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.R;
/** Observes the status of a specific Download */
public class DownloadObserver extends AsyncTask<Void, Void, Void> {
*//** Observes the status of a specific Download *//*
public class DownloadObserver{
*//******
private static final String TAG = "DownloadObserver";
/** Types of downloads to observe. */
/** Types of downloads to observe. *//*
public static final int TYPE_FEED = 0;
public static final int TYPE_IMAGE = 1;
public static final int TYPE_MEDIA = 2;
/** Error codes */
*//** Error codes *//*
public static final int ALREADY_DOWNLOADED = 1;
public static final int NO_DOWNLOAD_FOUND = 2;
@ -165,7 +166,7 @@ public class DownloadObserver extends AsyncTask<Void, Void, Void> {
}
/** Request a cursor with all running Feedfile downloads */
*//** Request a cursor with all running Feedfile downloads *//*
private Cursor getDownloadCursor() {
// Collect download ids
@ -186,7 +187,7 @@ public class DownloadObserver extends AsyncTask<Void, Void, Void> {
return result;
}
/** Return value of a specific column */
*//** Return value of a specific column *//*
private int getDownloadStatus(Cursor c, String column) {
int status = c.getInt(c.getColumnIndex(column));
return status;
@ -205,7 +206,7 @@ public class DownloadObserver extends AsyncTask<Void, Void, Void> {
return context;
}
/** Find a DownloadStatus entry by its FeedFile */
*//** Find a DownloadStatus entry by its FeedFile *//*
public DownloadStatus findDownloadStatus(FeedFile f) {
for (DownloadStatus status : statusList) {
if (status.feedfile == f) {
@ -238,3 +239,4 @@ public class DownloadObserver extends AsyncTask<Void, Void, Void> {
}
}
*/

View File

@ -14,6 +14,9 @@ public class DownloadStatus {
/** Unique id for storing the object in database. */
protected long id;
/** Used by DownloadService to check if the status has been updated. */
protected volatile boolean updateAvailable;
protected FeedFile feedfile;
protected int progressPercent;
protected long soFar;
@ -29,8 +32,8 @@ public class DownloadStatus {
}
/** Constructor for restoring Download status entries from DB. */
public DownloadStatus(long id, FeedFile feedfile, boolean successful, int reason,
Date completionDate) {
public DownloadStatus(long id, FeedFile feedfile, boolean successful,
int reason, Date completionDate) {
this.id = id;
this.feedfile = feedfile;
progressPercent = 100;
@ -41,11 +44,9 @@ public class DownloadStatus {
this.done = true;
this.completionDate = completionDate;
}
/** Constructor for creating new completed downloads. */
public DownloadStatus(FeedFile feedfile, int reason,
boolean successful) {
public DownloadStatus(FeedFile feedfile, int reason, boolean successful) {
this(0, feedfile, successful, reason, new Date());
}
@ -88,8 +89,49 @@ public class DownloadStatus {
public boolean isDone() {
return done;
}
public void setFeedfile(FeedFile feedfile) {
this.feedfile = feedfile;
}
public void setProgressPercent(int progressPercent) {
this.progressPercent = progressPercent;
}
public void setSoFar(long soFar) {
this.soFar = soFar;
}
public void setSize(long size) {
this.size = size;
}
public void setStatusMsg(int statusMsg) {
this.statusMsg = statusMsg;
}
public void setReason(int reason) {
this.reason = reason;
}
public void setSuccessful(boolean successful) {
this.successful = successful;
}
public void setDone(boolean done) {
this.done = done;
}
public void setCompletionDate(Date completionDate) {
this.completionDate = completionDate;
}
public boolean isUpdateAvailable() {
return updateAvailable;
}
public void setUpdateAvailable(boolean updateAvailable) {
this.updateAvailable = updateAvailable;
}
}

View File

@ -1,10 +1,9 @@
package de.danoeh.antennapod.feed;
/** Represents a component of a Feed that has to be downloaded*/
/** Represents a component of a Feed that has to be downloaded */
public abstract class FeedFile extends FeedComponent {
protected String file_url;
protected String download_url;
protected long downloadId; // temporary id given by the Android DownloadManager
protected boolean downloaded;
public FeedFile(String file_url, String download_url, boolean downloaded) {
@ -21,24 +20,19 @@ public abstract class FeedFile extends FeedComponent {
public String getFile_url() {
return file_url;
}
public void setFile_url(String file_url) {
this.file_url = file_url;
}
public String getDownload_url() {
return download_url;
}
public void setDownload_url(String download_url) {
this.download_url = download_url;
}
public long getDownloadId() {
return downloadId;
}
public void setDownloadId(long downloadId) {
this.downloadId = downloadId;
}
public boolean isDownloaded() {
return downloaded;
}
@ -46,10 +40,4 @@ public abstract class FeedFile extends FeedComponent {
public void setDownloaded(boolean downloaded) {
this.downloaded = downloaded;
}
public boolean isDownloading() {
return downloadId != 0;
}
}

View File

@ -37,6 +37,7 @@ public class FeedManager {
public static final String ACITON_FEED_LIST_UPDATE = "de.danoeh.antennapod.action.feed.feedlistUpdate";
public static final String ACTION_UNREAD_ITEMS_UPDATE = "de.danoeh.antennapod.action.feed.unreadItemsUpdate";
public static final String ACTION_QUEUE_UPDATE = "de.danoeh.antennapod.action.feed.queueUpdate";
public static final String ACTION_DOWNLOADLOG_UPDATE = "de.danoeh.antennapod.action.feed.downloadLogUpdate";
public static final String EXTRA_FEED_ITEM_ID = "de.danoeh.antennapod.extra.feed.feedItemId";
public static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feed.feedId";
@ -167,8 +168,7 @@ public class FeedManager {
imageFile.delete();
}
} else if (requester.isDownloadingFile(feed.getImage())) {
requester.cancelDownload(context, feed.getImage()
.getDownloadId());
requester.cancelDownload(context, feed.getImage());
}
// delete stored media files and mark them as read
for (FeedItem item : feed.getItems()) {
@ -184,8 +184,7 @@ public class FeedManager {
mediaFile.delete();
} else if (item.getMedia() != null
&& requester.isDownloadingFile(item.getMedia())) {
requester.cancelDownload(context, item.getMedia()
.getDownloadId());
requester.cancelDownload(context, item.getMedia());
}
}
@ -334,20 +333,34 @@ public class FeedManager {
public void addDownloadStatus(final Context context,
final DownloadStatus status) {
downloadLog.add(status);
dbExec.execute(new Runnable() {
contentChanger.post(new Runnable() {
@Override
public void run() {
PodDBAdapter adapter = new PodDBAdapter(context);
adapter.open();
downloadLog.add(status);
final DownloadStatus removedStatus;
if (downloadLog.size() > DOWNLOAD_LOG_SIZE) {
adapter.removeDownloadStatus(downloadLog.remove(0));
removedStatus = downloadLog.remove(0);
} else {
removedStatus = null;
}
adapter.setDownloadStatus(status);
adapter.close();
context.sendBroadcast(new Intent(ACTION_DOWNLOADLOG_UPDATE));
dbExec.execute(new Runnable() {
@Override
public void run() {
PodDBAdapter adapter = new PodDBAdapter(context);
adapter.open();
if (removedStatus != null) {
adapter.removeDownloadStatus(removedStatus);
}
adapter.setDownloadStatus(status);
adapter.close();
}
});
}
});
}
public void addQueueItem(final Context context, final FeedItem item) {

View File

@ -5,7 +5,7 @@ import de.danoeh.antennapod.adapter.FeedlistAdapter;
import de.danoeh.antennapod.asynctask.FeedRemover;
import de.danoeh.antennapod.dialog.ConfirmationDialog;
import de.danoeh.antennapod.feed.*;
import de.danoeh.antennapod.service.DownloadService;
import de.danoeh.antennapod.service.download.DownloadService;
import de.danoeh.antennapod.storage.DownloadRequester;
import de.danoeh.antennapod.util.menuhandler.FeedMenuHandler;
import de.danoeh.antennapod.AppConfig;
@ -140,15 +140,7 @@ public class FeedlistFragment extends SherlockFragment implements
@Override
public void run() {
if (intent.getAction().equals(
DownloadService.ACTION_DOWNLOAD_HANDLED)) {
int type = intent.getIntExtra(DownloadService.EXTRA_DOWNLOAD_TYPE, 0);
if (type == DownloadService.DOWNLOAD_TYPE_IMAGE) {
fla.notifyDataSetChanged();
}
} else {
fla.notifyDataSetChanged();
}
fla.notifyDataSetChanged();
}
});
}

View File

@ -27,7 +27,7 @@ import de.danoeh.antennapod.adapter.FeedItemlistAdapter;
import de.danoeh.antennapod.feed.Feed;
import de.danoeh.antennapod.feed.FeedItem;
import de.danoeh.antennapod.feed.FeedManager;
import de.danoeh.antennapod.service.DownloadService;
import de.danoeh.antennapod.service.download.DownloadService;
import de.danoeh.antennapod.storage.DownloadRequester;
import de.danoeh.antennapod.util.EpisodeFilter;
import de.danoeh.antennapod.util.menuhandler.FeedItemMenuHandler;

View File

@ -3,60 +3,60 @@
* to complete, then stops
* */
package de.danoeh.antennapod.service;
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.Date;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.PodcastApp;
import de.danoeh.antennapod.activity.DownloadActivity;
import de.danoeh.antennapod.activity.AudioplayerActivity;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.asynctask.DownloadObserver;
import de.danoeh.antennapod.asynctask.DownloadStatus;
import de.danoeh.antennapod.feed.*;
import de.danoeh.antennapod.service.PlaybackService.LocalBinder;
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.DownloadError;
import de.danoeh.antennapod.util.InvalidFeedException;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.app.DownloadManager;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.IBinder;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.database.Cursor;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.media.MediaPlayer;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.Debug;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.webkit.URLUtil;
import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.PodcastApp;
import de.danoeh.antennapod.activity.DownloadActivity;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.asynctask.DownloadStatus;
import de.danoeh.antennapod.feed.Feed;
import de.danoeh.antennapod.feed.FeedFile;
import de.danoeh.antennapod.feed.FeedImage;
import de.danoeh.antennapod.feed.FeedManager;
import de.danoeh.antennapod.feed.FeedMedia;
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.DownloadError;
import de.danoeh.antennapod.util.InvalidFeedException;
public class DownloadService extends Service {
private static final String TAG = "DownloadService";
@ -67,13 +67,27 @@ public class DownloadService extends Service {
* If the DownloadService receives this intent, it will execute
* queryDownloads()
*/
public static final String ACTION_NOTIFY_DOWNLOADS_CHANGED = "action.de.danoeh.antennapod.service.notifyDownloadsChanged";
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";
/** Is used for sending the delete intent for the report notification */
private static final String ACTION_REPORT_DELETED = "action.de.danoeh.antennapod.service.reportDeleted";
/** Extra for ACTION_CANCEL_DOWNLOAD */
public static final String EXTRA_DOWNLOAD_URL = "downloadUrl";
public static final String ACTION_DOWNLOAD_HANDLED = "action.de.danoeh.antennapod.service.download_handled";
/** True if handled feed has an image. */
public static final String EXTRA_FEED_HAS_IMAGE = "extra.de.danoeh.antennapod.service.feed_has_image";
/**
* 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";
public static final String EXTRA_IMAGE_DOWNLOAD_ID = "extra.de.danoeh.antennapod.service.image_download_id";
/** Extra for ACTION_ENQUEUE_DOWNLOAD intent. */
public static final String EXTRA_REQUEST = "request";
// Download types for ACTION_DOWNLOAD_HANDLED
public static final String EXTRA_DOWNLOAD_TYPE = "extra.de.danoeh.antennapod.service.downloadType";
@ -81,9 +95,11 @@ public class DownloadService extends Service {
public static final int DOWNLOAD_TYPE_MEDIA = 2;
public static final int DOWNLOAD_TYPE_IMAGE = 3;
private ArrayList<DownloadStatus> completedDownloads;
private CopyOnWriteArrayList<DownloadStatus> completedDownloads;
private ExecutorService syncExecutor;
private ExecutorService downloadExecutor;
private DownloadRequester requester;
private FeedManager manager;
private NotificationCompat.Builder notificationBuilder;
@ -91,16 +107,14 @@ public class DownloadService extends Service {
private int REPORT_ID = 3;
/** Needed to determine the duration of a media file */
private MediaPlayer mediaplayer;
private DownloadManager downloadManager;
private DownloadObserver downloadObserver;
private List<Downloader> downloads;
private volatile boolean shutdownInitiated = false;
/** True if service is running. */
public static boolean isRunning = false;
/** Is started when service waits for shutdown. */
private Thread waiter;
private Handler handler;
private final IBinder mBinder = new LocalBinder();
@ -112,11 +126,10 @@ public class DownloadService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (waiter != null) {
waiter.interrupt();
if (intent.getParcelableExtra(EXTRA_REQUEST) != null) {
onDownloadQueued(intent);
}
queryDownloads();
return super.onStartCommand(intent, flags, startId);
return Service.START_NOT_STICKY;
}
@SuppressLint("NewApi")
@ -125,10 +138,18 @@ public class DownloadService extends Service {
if (AppConfig.DEBUG)
Log.d(TAG, "Service started");
isRunning = true;
completedDownloads = new ArrayList<DownloadStatus>();
registerReceiver(downloadReceiver, createIntentFilter());
registerReceiver(onDownloadsChanged, new IntentFilter(
ACTION_NOTIFY_DOWNLOADS_CHANGED));
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);
registerReceiver(reportDeleted, new IntentFilter(ACTION_REPORT_DELETED));
syncExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
@Override
@ -147,17 +168,20 @@ public class DownloadService extends Service {
return t;
}
});
downloadExecutor = Executors.newFixedThreadPool(2, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setPriority(Thread.MIN_PRIORITY);
return t;
}
});
manager = FeedManager.getInstance();
requester = DownloadRequester.getInstance();
mediaplayer = new MediaPlayer();
downloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
downloadObserver = new DownloadObserver(this);
setupNotification();
if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
downloadObserver.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else {
downloadObserver.execute();
}
}
@Override
@ -171,46 +195,8 @@ public class DownloadService extends Service {
Log.d(TAG, "Service shutting down");
isRunning = false;
mediaplayer.release();
unregisterReceiver(downloadReceiver);
unregisterReceiver(onDownloadsChanged);
downloadObserver.cancel(true);
createReport();
}
private IntentFilter createIntentFilter() {
IntentFilter filter = new IntentFilter();
filter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
return filter;
}
/** Shuts down Executor service and prepares for shutdown */
private void initiateShutdown() {
if (AppConfig.DEBUG)
Log.d(TAG, "Initiating shutdown");
// Wait until PoolExecutor is done
waiter = new Thread() {
@Override
public void run() {
syncExecutor.shutdown();
try {
if (AppConfig.DEBUG)
Log.d(TAG, "Starting to wait for termination");
boolean b = syncExecutor.awaitTermination(20L,
TimeUnit.SECONDS);
if (AppConfig.DEBUG)
Log.d(TAG,
"Stopping waiting for termination; Result : "
+ b);
stopForeground(true);
stopSelf();
} catch (InterruptedException e) {
e.printStackTrace();
Log.i(TAG, "Service shutdown was interrupted.");
shutdownInitiated = false;
}
}
};
waiter.start();
unregisterReceiver(cancelDownloadReceiver);
unregisterReceiver(downloadQueued);
}
private void setupNotification() {
@ -232,104 +218,164 @@ public class DownloadService extends Service {
Log.d(TAG, "Notification set up");
}
private BroadcastReceiver onDownloadsChanged = new BroadcastReceiver() {
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_NOTIFY_DOWNLOADS_CHANGED)) {
queryDownloads();
}
}
};
private BroadcastReceiver downloadReceiver = new BroadcastReceiver() {
@SuppressLint("NewApi")
@Override
public void onReceive(final Context context, final Intent intent) {
AsyncTask<Void, Void, Void> handler = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
int status = -1;
String file_url = null;
boolean successful = false;
int reason = 0;
if (AppConfig.DEBUG)
Log.d(TAG, "Received 'Download Complete' - message.");
long downloadId = intent.getLongExtra(
DownloadManager.EXTRA_DOWNLOAD_ID, 0);
// get status
DownloadManager.Query q = new DownloadManager.Query();
q.setFilterById(downloadId);
Cursor c = downloadManager.query(q);
if (c.moveToFirst()) {
status = c.getInt(c
.getColumnIndex(DownloadManager.COLUMN_STATUS));
String uriString = c
.getString(c
.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
if (uriString != null) {
Uri file_uri = Uri.parse(uriString);
file_url = file_uri.getPath();
} else {
Log.w(TAG,
"DownloadManager didn't provide a destination URI for downloaded file");
}
if (AppConfig.DEBUG)
Log.d(TAG, "File url given by download manager is "
+ file_url);
}
if (downloadId == 0) {
Log.d(TAG, "Download ID was null");
}
FeedFile download = requester.getFeedFile(downloadId);
if (download != null) {
if (status == DownloadManager.STATUS_SUCCESSFUL) {
if (file_url != null) {
download.setFile_url(file_url);
}
if (download.getClass() == Feed.class) {
handleCompletedFeedDownload(context,
(Feed) download);
} else if (download.getClass() == FeedImage.class) {
handleCompletedImageDownload(context,
(FeedImage) download);
} else if (download.getClass() == FeedMedia.class) {
handleCompletedFeedMediaDownload(context,
(FeedMedia) download);
}
successful = true;
} else if (status == DownloadManager.STATUS_FAILED) {
reason = c
.getInt(c
.getColumnIndex(DownloadManager.COLUMN_REASON));
Log.e(TAG, "Download failed");
Log.e(TAG, "reason code is " + reason);
successful = false;
saveDownloadStatus(new DownloadStatus(download,
reason, successful));
requester.removeDownload(download);
sendDownloadHandledIntent(download.getDownloadId(),
false, 0, getDownloadType(download));
download.setDownloadId(0);
}
queryDownloads();
}
c.close();
return null;
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 (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
handler.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else {
handler.execute();
if (AppConfig.DEBUG)
Log.d(TAG, "Cancelling download with url " + url);
Downloader d = getDownloader(url);
if (d != null) {
d.interrupt();
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.interrupt();
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;
setupNotification();
}
DownloadRequester requester = DownloadRequester.getInstance();
FeedFile feedfile = requester.getDownload(request.source);
if (feedfile != null) {
DownloadStatus status = new DownloadStatus(feedfile);
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(this, 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>() {
@Override
protected Void doInBackground(Void... params) {
if (AppConfig.DEBUG)
Log.d(TAG, "Received 'Download Complete' - message.");
DownloadStatus status = downloader.getStatus();
status.setCompletionDate(new Date());
boolean successful = status.isSuccessful();
int reason = status.getReason();
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 {
if (!successful
&& reason != DownloadError.ERROR_DOWNLOAD_CANCELLED) {
Log.e(TAG, "Download failed");
}
saveDownloadStatus(status);
sendDownloadHandledIntent(getDownloadType(download));
}
}
removeDownload(downloader);
if (!successful) {
queryDownloads();
}
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) {
downloads.remove(d);
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
@ -355,41 +401,32 @@ public class DownloadService extends Service {
}
}
private void sendDownloadHandledIntent(long downloadId,
boolean feedHasImage, long imageDownloadId, int type) {
private void sendDownloadHandledIntent(int type) {
Intent intent = new Intent(ACTION_DOWNLOAD_HANDLED);
intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId);
intent.putExtra(EXTRA_FEED_HAS_IMAGE, feedHasImage);
intent.putExtra(EXTRA_DOWNLOAD_TYPE, type);
if (feedHasImage) {
intent.putExtra(EXTRA_IMAGE_DOWNLOAD_ID, imageDownloadId);
}
sendBroadcast(intent);
}
private BroadcastReceiver reportDeleted = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(ACTION_REPORT_DELETED)) {
completedDownloads.clear();
}
}
};
/**
* 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 feeds is > 1 or if at least one media file was
* downloaded.
*/
private void createReport() {
private void updateReport() {
// check if report should be created
boolean createReport = false;
int feedCount = 0;
for (DownloadStatus status : completedDownloads) {
if (status.getFeedFile().getClass() == Feed.class) {
feedCount++;
if (feedCount > 1) {
createReport = true;
break;
}
} else if (status.getFeedFile().getClass() == FeedMedia.class) {
createReport = true;
break;
}
}
if (createReport) {
if (!completedDownloads.isEmpty()) {
if (AppConfig.DEBUG)
Log.d(TAG, "Creating report");
int successfulDownloads = 0;
@ -417,7 +454,12 @@ public class DownloadService extends Service {
.setContentIntent(
PendingIntent.getActivity(this, 0, new Intent(this,
MainActivity.class), 0))
.setAutoCancel(true).getNotification();
.setAutoCancel(true)
.setDeleteIntent(
PendingIntent.getBroadcast(this, 0, new Intent(
ACTION_REPORT_DELETED),
PendingIntent.FLAG_UPDATE_CURRENT))
.getNotification();
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify(REPORT_ID, notification);
@ -428,11 +470,18 @@ public class DownloadService extends Service {
}
/** Check if there's something else to download, otherwise stop */
private void queryDownloads() {
int numOfDownloads = requester.getNumberOfDownloads();
if (!shutdownInitiated && numOfDownloads == 0) {
void queryDownloads() {
int numOfDownloads = downloads.size();
if (AppConfig.DEBUG)
Log.d(TAG, numOfDownloads + " downloads left");
if (AppConfig.DEBUG)
Log.d(TAG, "ShutdownInitiated: " + shutdownInitiated);
if (numOfDownloads == 0) {
if (AppConfig.DEBUG)
Log.d(TAG, "Starting shutdown");
shutdownInitiated = true;
initiateShutdown();
stopForeground(true);
} else {
// update notification
notificationBuilder.setContentText(numOfDownloads
@ -440,29 +489,29 @@ public class DownloadService extends Service {
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify(NOTIFICATION_ID, notificationBuilder.getNotification());
}
updateReport();
}
/** Is called whenever a Feed is downloaded */
private void handleCompletedFeedDownload(Context context, Feed feed) {
private void handleCompletedFeedDownload(DownloadStatus status) {
if (AppConfig.DEBUG)
Log.d(TAG, "Handling completed Feed Download");
syncExecutor.execute(new FeedSyncThread(feed, this));
syncExecutor.execute(new FeedSyncThread(status));
}
/** Is called whenever a Feed-Image is downloaded */
private void handleCompletedImageDownload(Context context, FeedImage image) {
private void handleCompletedImageDownload(DownloadStatus status) {
if (AppConfig.DEBUG)
Log.d(TAG, "Handling completed Image Download");
syncExecutor.execute(new ImageHandlerThread(image, this));
syncExecutor.execute(new ImageHandlerThread(status));
}
/** Is called whenever a FeedMedia is downloaded. */
private void handleCompletedFeedMediaDownload(Context context,
FeedMedia media) {
private void handleCompletedFeedMediaDownload(DownloadStatus status) {
if (AppConfig.DEBUG)
Log.d(TAG, "Handling completed FeedMedia Download");
syncExecutor.execute(new MediaHandlerThread(media, this));
syncExecutor.execute(new MediaHandlerThread(status));
}
/**
@ -473,21 +522,18 @@ public class DownloadService extends Service {
private static final String TAG = "FeedSyncThread";
private Feed feed;
private DownloadService service;
private DownloadStatus status;
private int reason;
private boolean successful;
public FeedSyncThread(Feed feed, DownloadService service) {
this.feed = feed;
this.service = service;
public FeedSyncThread(DownloadStatus status) {
this.feed = (Feed) status.getFeedFile();
this.status = status;
}
public void run() {
Feed savedFeed = null;
long imageId = 0;
boolean hasImage = false;
long downloadId = feed.getDownloadId();
reason = 0;
successful = true;
FeedManager manager = FeedManager.getInstance();
@ -501,18 +547,16 @@ public class DownloadService extends Service {
if (checkFeedData(feed) == false) {
throw new InvalidFeedException();
}
feed.setDownloadId(0);
// Save information of feed in DB
savedFeed = manager.updateFeed(service, feed);
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);
imageId = requester.downloadImage(service,
requester.downloadImage(DownloadService.this,
savedFeed.getImage());
hasImage = true;
}
} catch (SAXException e) {
@ -537,14 +581,13 @@ public class DownloadService extends Service {
reason = DownloadError.ERROR_PARSER_EXCEPTION;
}
requester.removeDownload(feed);
// cleanup();
if (savedFeed == null) {
savedFeed = feed;
}
saveDownloadStatus(new DownloadStatus(savedFeed, reason, successful));
sendDownloadHandledIntent(downloadId, hasImage, imageId,
DOWNLOAD_TYPE_FEED);
sendDownloadHandledIntent(DOWNLOAD_TYPE_FEED);
queryDownloads();
}
@ -579,25 +622,22 @@ public class DownloadService extends Service {
/** Handles a completed image download. */
class ImageHandlerThread implements Runnable {
private FeedImage image;
private DownloadService service;
private DownloadStatus status;
public ImageHandlerThread(FeedImage image, DownloadService service) {
this.image = image;
this.service = service;
public ImageHandlerThread(DownloadStatus status) {
this.image = (FeedImage) status.getFeedFile();
this.status = status;
}
@Override
public void run() {
image.setDownloaded(true);
requester.removeDownload(image);
saveDownloadStatus(new DownloadStatus(image, 0, true));
sendDownloadHandledIntent(image.getDownloadId(), false, 0,
DOWNLOAD_TYPE_IMAGE);
image.setDownloadId(0);
manager.setFeedImage(service, image);
saveDownloadStatus(status);
sendDownloadHandledIntent(DOWNLOAD_TYPE_IMAGE);
manager.setFeedImage(DownloadService.this, image);
if (image.getFeed() != null) {
manager.setFeed(service, image.getFeed());
manager.setFeed(DownloadService.this, image.getFeed());
} else {
Log.e(TAG,
"Image has no feed, image might not be saved correctly!");
@ -609,17 +649,16 @@ public class DownloadService extends Service {
/** Handles a completed media download. */
class MediaHandlerThread implements Runnable {
private FeedMedia media;
private DownloadService service;
private DownloadStatus status;
public MediaHandlerThread(FeedMedia media, DownloadService service) {
public MediaHandlerThread(DownloadStatus status) {
super();
this.media = media;
this.service = service;
this.media = (FeedMedia) status.getFeedFile();
this.status = status;
}
@Override
public void run() {
requester.removeDownload(media);
media.setDownloaded(true);
// Get duration
try {
@ -632,11 +671,10 @@ public class DownloadService extends Service {
if (AppConfig.DEBUG)
Log.d(TAG, "Duration of file is " + media.getDuration());
mediaplayer.reset();
saveDownloadStatus(new DownloadStatus(media, 0, true));
sendDownloadHandledIntent(media.getDownloadId(), false, 0,
DOWNLOAD_TYPE_MEDIA);
media.setDownloadId(0);
manager.setFeedMedia(service, media);
saveDownloadStatus(status);
sendDownloadHandledIntent(DOWNLOAD_TYPE_MEDIA);
manager.setFeedMedia(DownloadService.this, media);
boolean autoQueue = PreferenceManager.getDefaultSharedPreferences(
getApplicationContext()).getBoolean(
PodcastApp.PREF_AUTO_QUEUE, true);
@ -655,13 +693,59 @@ public class DownloadService extends Service {
if (AppConfig.DEBUG)
Log.d(TAG, "Item is already in queue");
}
queryDownloads();
}
}
public DownloadObserver getDownloadObserver() {
return downloadObserver;
/** 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;
}
}
public List<Downloader> getDownloads() {
return downloads;
}
}

View File

@ -0,0 +1,59 @@
package de.danoeh.antennapod.service.download;
import de.danoeh.antennapod.asynctask.DownloadStatus;
import android.os.Environment;
import android.os.Handler;
import android.os.StatFs;
/** Downloads files */
public abstract class Downloader extends Thread {
private static final String TAG = "Downloader";
private Handler handler;
private DownloadService downloadService;
protected boolean finished;
protected volatile DownloadStatus status;
public Downloader(DownloadService downloadService, DownloadStatus status) {
super();
this.downloadService = downloadService;
this.status = status;
handler = new Handler();
}
/**
* This method must be called when the download was completed, failed, or
* was cancelled
*/
protected void finish() {
if (!finished) {
finished = true;
handler.post(new Runnable() {
@Override
public void run() {
downloadService.onDownloadCompleted(Downloader.this);
}
});
}
}
protected void publishProgress() {
status.setUpdateAvailable(true);
}
protected abstract void download();
@Override
public final void run() {
download();
finish();
}
public DownloadStatus getStatus() {
return status;
}
}

View File

@ -0,0 +1,136 @@
package de.danoeh.antennapod.service.download;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
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.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import android.util.Log;
import de.danoeh.antennapod.AppConfig;
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 BUFFER_SIZE = 8 * 1024;
private static final int CONNECTION_TIMEOUT = 5000;
public HttpDownloader(DownloadService downloadService, DownloadStatus status) {
super(downloadService, status);
}
@Override
protected void download() {
HttpURLConnection connection = null;
OutputStream out = null;
try {
status.setStatusMsg(R.string.download_pending);
publishProgress();
URL url = new URL(status.getFeedFile().getDownload_url());
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(CONNECTION_TIMEOUT);
if (AppConfig.DEBUG) {
Log.d(TAG, "Connected to resource");
}
if (StorageUtils.externalStorageMounted()) {
File destination = new File(status.getFeedFile().getFile_url());
if (!destination.exists()) {
InputStream in = new BufferedInputStream(
connection.getInputStream());
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(connection.getContentLength());
if (AppConfig.DEBUG)
Log.d(TAG, "Size is " + status.getSize());
if (status.getSize() == -1
|| status.getSize() <= StorageUtils
.getFreeSpaceAvailable()) {
if (AppConfig.DEBUG)
Log.d(TAG, "Size is " + status.getSize());
publishProgress();
if (AppConfig.DEBUG)
Log.d(TAG, "Starting download");
while ((count = in.read(buffer)) != -1
&& !isInterrupted()) {
out.write(buffer, 0, count);
status.setSoFar(status.getSoFar() + count);
status.setProgressPercent((int) (((double) status
.getSoFar() / (double) status.getSize()) * 100));
}
if (isInterrupted()) {
onCancelled();
} else {
onSuccess();
}
} else {
onFail(DownloadError.ERROR_NOT_ENOUGH_SPACE);
}
} else {
onFail(DownloadError.ERROR_FILE_EXISTS);
}
} else {
onFail(DownloadError.ERROR_DEVICE_NOT_FOUND);
}
} catch (MalformedURLException e) {
e.printStackTrace();
onFail(DownloadError.ERROR_MALFORMED_URL);
} catch (SocketTimeoutException e) {
e.printStackTrace();
onFail(DownloadError.ERROR_CONNECTION_ERROR);
} catch (IOException e) {
e.printStackTrace();
onFail(DownloadError.ERROR_IO_ERROR);
} finally {
if (connection != null) {
connection.disconnect();
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private void onSuccess() {
if (AppConfig.DEBUG)
Log.d(TAG, "Download was successful");
status.setSuccessful(true);
status.setDone(true);
}
private void onFail(int reason) {
if (AppConfig.DEBUG) {
Log.d(TAG, "Download failed");
}
status.setReason(reason);
status.setDone(true);
status.setSuccessful(false);
}
private void onCancelled() {
if (AppConfig.DEBUG)
Log.d(TAG, "Download was cancelled");
status.setReason(DownloadError.ERROR_DOWNLOAD_CANCELLED);
status.setDone(true);
status.setSuccessful(false);
}
}

View File

@ -1,17 +1,11 @@
package de.danoeh.antennapod.storage;
import java.io.File;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import android.annotation.SuppressLint;
import android.app.DownloadManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.net.Uri;
import android.os.IBinder;
import android.util.Log;
import android.webkit.URLUtil;
import de.danoeh.antennapod.AppConfig;
@ -19,31 +13,28 @@ import de.danoeh.antennapod.feed.Feed;
import de.danoeh.antennapod.feed.FeedFile;
import de.danoeh.antennapod.feed.FeedImage;
import de.danoeh.antennapod.feed.FeedMedia;
import de.danoeh.antennapod.service.DownloadService;
import de.danoeh.antennapod.service.download.DownloadService;
import de.danoeh.antennapod.util.NumberGenerator;
import de.danoeh.antennapod.util.URLChecker;
public class DownloadRequester {// TODO handle externalstorage missing
public class DownloadRequester {
private static final String TAG = "DownloadRequester";
private static final int currentApi = android.os.Build.VERSION.SDK_INT;
public static String EXTRA_DOWNLOAD_ID = "extra.de.danoeh.antennapod.storage.download_id";
public static String EXTRA_ITEM_ID = "extra.de.danoeh.antennapod.storage.item_id";
public static String ACTION_DOWNLOAD_QUEUED = "action.de.danoeh.antennapod.storage.downloadQueued";
private static boolean STORE_ON_SD = true;
public static String IMAGE_DOWNLOADPATH = "images/";
public static String FEED_DOWNLOADPATH = "cache/";
public static String MEDIA_DOWNLOADPATH = "media/";
private static DownloadRequester downloader;
private DownloadManager manager;
private List<FeedFile> downloads;
Map<String, FeedFile> downloads;
private DownloadRequester() {
downloads = new CopyOnWriteArrayList<FeedFile>();
downloads = new ConcurrentHashMap<String, FeedFile>();
}
public static DownloadRequester getInstance() {
@ -53,8 +44,7 @@ public class DownloadRequester {// TODO handle externalstorage missing
return downloader;
}
@SuppressLint("NewApi")
private long download(Context context, FeedFile item, File dest) {
private void download(Context context, FeedFile item, File dest) {
if (!isDownloadingFile(item)) {
if (dest.exists()) {
if (AppConfig.DEBUG)
@ -64,106 +54,75 @@ public class DownloadRequester {// TODO handle externalstorage missing
if (AppConfig.DEBUG)
Log.d(TAG,
"Requesting download of url " + item.getDownload_url());
downloads.add(item);
item.setDownload_url(URLChecker.prepareURL(item.getDownload_url()));
DownloadManager.Request request = new DownloadManager.Request(
Uri.parse(item.getDownload_url())).setDestinationUri(Uri
.fromFile(dest));
if (AppConfig.DEBUG)
Log.d(TAG, "Version is " + currentApi);
if (currentApi >= 11) {
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN);
} else {
request.setVisibleInDownloadsUi(false);
request.setShowRunningNotification(false);
}
// TODO Set Allowed Network Types
DownloadManager manager = (DownloadManager) context
.getSystemService(Context.DOWNLOAD_SERVICE);
long downloadId = manager.enqueue(request);
item.setDownloadId(downloadId);
item.setFile_url(dest.toString());
context.startService(new Intent(context, DownloadService.class));
downloads.put(item.getDownload_url(), item);
DownloadService.Request request = new DownloadService.Request(
item.getFile_url(), item.getDownload_url());
if (!DownloadService.isRunning) {
Intent launchIntent = new Intent(context, DownloadService.class);
launchIntent.putExtra(DownloadService.EXTRA_REQUEST, request);
context.startService(launchIntent);
} else {
Intent queueIntent = new Intent(
DownloadService.ACTION_ENQUEUE_DOWNLOAD);
queueIntent.putExtra(DownloadService.EXTRA_REQUEST, request);
context.sendBroadcast(queueIntent);
}
context.sendBroadcast(new Intent(ACTION_DOWNLOAD_QUEUED));
return downloadId;
} else {
Log.e(TAG, "URL " + item.getDownload_url()
+ " is already being downloaded");
return 0;
}
}
public long downloadFeed(Context context, Feed feed) {
return download(context, feed, new File(getFeedfilePath(context),
public void downloadFeed(Context context, Feed feed) {
download(context, feed, new File(getFeedfilePath(context),
getFeedfileName(feed)));
}
public long downloadImage(Context context, FeedImage image) {
return download(context, image, new File(getImagefilePath(context),
public void downloadImage(Context context, FeedImage image) {
download(context, image, new File(getImagefilePath(context),
getImagefileName(image)));
}
public long downloadMedia(Context context, FeedMedia feedmedia) {
return download(context, feedmedia,
public void downloadMedia(Context context, FeedMedia feedmedia) {
download(context, feedmedia,
new File(getMediafilePath(context, feedmedia),
getMediafilename(feedmedia)));
}
/**
* Cancels a running download.
*
* @param context
* A context needed to get the DownloadManager service
* @param id
* ID of the download to cancel
* */
public void cancelDownload(final Context context, final long id) {
public void cancelDownload(final Context context, final FeedFile f) {
cancelDownload(context, f.getDownload_url());
}
/**
* Cancels a running download.
* */
public void cancelDownload(final Context context, final String downloadUrl) {
if (AppConfig.DEBUG)
Log.d(TAG, "Cancelling download with id " + id);
DownloadManager dm = (DownloadManager) context
.getSystemService(Context.DOWNLOAD_SERVICE);
int removed = dm.remove(id);
if (removed > 0) {
FeedFile f = getFeedFile(id);
if (f != null) {
downloads.remove(f);
f.setFile_url(null);
f.setDownloadId(0);
}
notifyDownloadService(context);
}
Log.d(TAG, "Cancelling download with url " + downloadUrl);
Intent cancelIntent = new Intent(DownloadService.ACTION_CANCEL_DOWNLOAD);
cancelIntent.putExtra(DownloadService.EXTRA_DOWNLOAD_URL, downloadUrl);
context.sendBroadcast(cancelIntent);
}
/** Cancels all running downloads */
public void cancelAllDownloads(Context context) {
if (AppConfig.DEBUG)
Log.d(TAG, "Cancelling all running downloads");
DownloadManager dm = (DownloadManager) context
.getSystemService(Context.DOWNLOAD_SERVICE);
for (FeedFile f : downloads) {
dm.remove(f.getDownloadId());
f.setFile_url(null);
f.setDownloadId(0);
}
downloads.clear();
notifyDownloadService(context);
}
/** Get a feedfile by its download id */
public FeedFile getFeedFile(long id) {
for (FeedFile f : downloads) {
if (f.getDownloadId() == id) {
return f;
}
}
return null;
context.sendBroadcast(new Intent(
DownloadService.ACTION_CANCEL_ALL_DOWNLOADS));
}
/** Returns true if there is at least one Feed in the downloads queue. */
public boolean isDownloadingFeeds() {
for (FeedFile f : downloads) {
for (FeedFile f : downloads.values()) {
if (f.getClass() == Feed.class) {
return true;
}
@ -173,22 +132,19 @@ public class DownloadRequester {// TODO handle externalstorage missing
/** Checks if feedfile is in the downloads list */
public boolean isDownloadingFile(FeedFile item) {
for (FeedFile f : downloads) {
if (f.getDownload_url().equals(item.getDownload_url())) {
return true;
}
if (item.getDownload_url() != null) {
return downloads.containsKey(item.getDownload_url());
}
return false;
}
public FeedFile getDownload(String downloadUrl) {
return downloads.get(downloadUrl);
}
/** Checks if feedfile with the given download url is in the downloads list */
public boolean isDownloadingFile(String downloadUrl) {
for (FeedFile f : downloads) {
if (f.getDownload_url().equals(downloadUrl)) {
return true;
}
}
return false;
return downloads.get(downloadUrl) != null;
}
public boolean hasNoDownloads() {
@ -201,11 +157,9 @@ public class DownloadRequester {// TODO handle externalstorage missing
/** Remove an object from the downloads-list of the requester. */
public void removeDownload(FeedFile f) {
downloads.remove(f);
}
public List<FeedFile> getDownloads() {
return downloads;
if (downloads.remove(f.getDownload_url()) == null) {
Log.e(TAG, "Could not remove object with url " + f.getDownload_url());
}
}
/** Get the number of uncompleted Downloads */
@ -241,8 +195,4 @@ public class DownloadRequester {// TODO handle externalstorage missing
media.getMime_type());
}
/** Notifies the DownloadService to check if there are any Downloads left */
public void notifyDownloadService(Context context) {
context.sendBroadcast(new Intent(DownloadService.ACTION_NOTIFY_DOWNLOADS_CHANGED));
}
}

View File

@ -10,49 +10,52 @@ import android.util.Log;
/** Parses several date formats. */
public class SyndDateUtils {
private static final String TAG = "DateUtils";
public static final String RFC822 = "dd MMM yyyy HH:mm:ss Z";
/** RFC 822 date format with day of the week. */
public static final String RFC822DAY = "EEE, " + RFC822;
public static final String[] RFC822DATES = { "EEE, dd MMM yyyy HH:mm:ss Z",
"dd MMM yyyy HH:mm:ss Z", "EEE, dd MMM yy HH:mm:ss Z",
"dd MMM yy HH:mm:ss Z", "EEE, dd MMM yyyy HH:mm:ss z",
"dd MMM yyyy HH:mm:ss z", "EEE, dd MMM yy HH:mm:ss z",
"dd MMM yy HH:mm:ss z" };
/** RFC 3339 date format for UTC dates. */
public static final String RFC3339UTC = "yyyy-MM-dd'T'HH:mm:ss'Z'";
/** RFC 3339 date format for localtime dates with offset. */
public static final String RFC3339LOCAL = "yyyy-MM-dd'T'HH:mm:ssZ";
private static ThreadLocal<SimpleDateFormat> RFC822Formatter = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat(RFC822DAY, Locale.US);
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat(RFC822DATES[0], Locale.US);
}
};
private static ThreadLocal<SimpleDateFormat> RFC3339Formatter = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat(RFC3339UTC, Locale.US);
}
};
public static Date parseRFC822Date(final String date) {
private static ThreadLocal<SimpleDateFormat> RFC3339Formatter = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat(RFC3339UTC, Locale.US);
}
};
public static Date parseRFC822Date(String date) {
Date result = null;
if (date.contains("PDT")) {
date = date.replace("PDT", "PST8PDT");
}
SimpleDateFormat format = RFC822Formatter.get();
try {
result = format.parse(date);
} catch (ParseException e) {
e.printStackTrace();
format.applyPattern(RFC822);
for (int i = 0; i < RFC822DATES.length; i++) {
try {
result = format.parse(date);
} catch (ParseException e1) {
e1.printStackTrace();
Log.e(TAG, "Unable to parse feed date correctly");
} finally {
format.applyPattern(RFC822DAY); // apply old pattern again
result = format.parse(date);
break;
} catch (ParseException e) {
e.printStackTrace();
}
}
if (result == null) {
Log.e(TAG, "Unable to parse feed date correctly");
}
return result;
}
@ -90,7 +93,11 @@ public class SyndDateUtils {
return result;
}
/** Takes a string of the form [HH:]MM:SS[.mmm] and converts it to milliseconds. */
/**
* Takes a string of the form [HH:]MM:SS[.mmm] and converts it to
* milliseconds.
*/
public static long parseTimeString(final String time) {
String[] parts = time.split(":");
long result = 0;
@ -102,7 +109,7 @@ public class SyndDateUtils {
}
result += Integer.valueOf(parts[idx]) * 60000;
idx++;
result += ( Float.valueOf(parts[idx])) * 1000;
result += (Float.valueOf(parts[idx])) * 1000;
return result;
}
}

View File

@ -1,27 +1,35 @@
package de.danoeh.antennapod.util;
import de.danoeh.antennapod.R;
import android.app.DownloadManager;
import android.content.Context;
import de.danoeh.antennapod.R;
/** Utility class for Download Errors. */
public class DownloadError {
public static final int ERROR_PARSER_EXCEPTION = 1;
public static final int ERROR_UNSUPPORTED_TYPE = 2;
public static final int ERROR_CONNECTION_ERROR = 3;
public static final int ERROR_MALFORMED_URL = 4;
public static final int ERROR_IO_ERROR = 5;
public static final int ERROR_FILE_EXISTS = 6;
public static final int ERROR_DOWNLOAD_CANCELLED = 7;
public static final int ERROR_DEVICE_NOT_FOUND = 8;
public static final int ERROR_HTTP_DATA_ERROR = 9;
public static final int ERROR_NOT_ENOUGH_SPACE = 10;
/** Get a human-readable string for a specific error code. */
public static String getErrorString(Context context, int code) {
int resId;
switch(code) {
case DownloadManager.ERROR_DEVICE_NOT_FOUND:
case ERROR_NOT_ENOUGH_SPACE:
resId = R.string.download_error_insufficient_space;
break;
case DownloadManager.ERROR_FILE_ERROR:
resId = R.string.download_error_file_error;
case ERROR_DEVICE_NOT_FOUND:
resId = R.string.download_error_device_not_found;
break;
case DownloadManager.ERROR_HTTP_DATA_ERROR:
case ERROR_IO_ERROR:
resId = R.string.download_error_io_error;
break;
case ERROR_HTTP_DATA_ERROR:
resId = R.string.download_error_http_data_error;
break;
case ERROR_PARSER_EXCEPTION:

View File

@ -4,6 +4,7 @@ import de.danoeh.antennapod.activity.StorageErrorActivity;
import android.app.Activity;
import android.content.Intent;
import android.os.Environment;
import android.os.StatFs;
/** Utility functions for handling storage errors */
public class StorageUtils {
@ -25,4 +26,14 @@ public class StorageUtils {
}
return storageAvailable;
}
/** Get the number of free bytes that are available on the external storage. */
public static int getFreeSpaceAvailable() {
StatFs stat = new StatFs(Environment.getExternalStorageDirectory().getPath());
return stat.getAvailableBlocks() * stat.getBlockSize();
}
public static boolean externalStorageMounted() {
return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
}
}

View File

@ -81,8 +81,7 @@ public class FeedItemMenuHandler {
manager.deleteFeedMedia(context, selectedItem.getMedia());
break;
case R.id.cancel_download_item:
requester.cancelDownload(context, selectedItem.getMedia()
.getDownloadId());
requester.cancelDownload(context, selectedItem.getMedia());
break;
case R.id.mark_read_item:
manager.markItemRead(context, selectedItem, true);

View File

@ -15,7 +15,7 @@ import de.danoeh.antennapod.asynctask.FlattrClickWorker;
import de.danoeh.antennapod.feed.Feed;
import de.danoeh.antennapod.feed.FeedItem;
import de.danoeh.antennapod.feed.FeedManager;
import de.danoeh.antennapod.service.DownloadService;
import de.danoeh.antennapod.service.download.DownloadService;
import de.danoeh.antennapod.storage.DownloadRequester;
import de.danoeh.antennapod.util.ShareUtils;
import de.danoeh.antennapod.AppConfig;