diff options
author | daniel oeh <daniel.oeh@gmail.com> | 2013-08-03 13:58:31 +0200 |
---|---|---|
committer | daniel oeh <daniel.oeh@gmail.com> | 2013-08-03 13:58:31 +0200 |
commit | 9ba3dc0d823b786700010a60d38f47c802101d27 (patch) | |
tree | 89b74eff993fdcb3b373c64695c72d534f1a80cd /src | |
parent | 2071793e6aa1a106744078fcbcf7c0529ed315c4 (diff) | |
download | AntennaPod-9ba3dc0d823b786700010a60d38f47c802101d27.zip |
Improved DownloadService, several bugfixes
- DownloadService should now terminate properly as soon as all downloads have been completed.
- Notification bug ("0 downloads left" notification) should be fixed
Diffstat (limited to 'src')
10 files changed, 1258 insertions, 1171 deletions
diff --git a/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java b/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java index deb4602bb..50ff38e22 100644 --- a/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java +++ b/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java @@ -122,9 +122,11 @@ public abstract class OnlineFeedViewActivity extends SherlockFragmentActivity { feed.setFile_url(fileUrl); DownloadRequest request = new DownloadRequest(feed.getFile_url(), feed.getDownload_url(), "OnlineFeed", 0, Feed.FEEDFILETYPE_FEED); + /* TODO update HttpDownloader httpDownloader = new HttpDownloader(downloaderCallback, request); httpDownloader.start(); + */ } /** Displays a progress indicator. */ diff --git a/src/de/danoeh/antennapod/feed/FeedManager.java b/src/de/danoeh/antennapod/feed/FeedManager.java index f16756617..343340a98 100644 --- a/src/de/danoeh/antennapod/feed/FeedManager.java +++ b/src/de/danoeh/antennapod/feed/FeedManager.java @@ -9,7 +9,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; -import java.util.TreeSet; import java.util.Comparator; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -1448,7 +1447,7 @@ public class FeedManager { long imageIndex = feedlistCursor .getLong(PodDBAdapter.KEY_IMAGE_INDEX); if (imageIndex != 0) { - feed.setImage(DBReader.getFeedImage(adapter, imageIndex)); + feed.setImage(DBReader.getFeedImage(context, imageIndex)); feed.getImage().setFeed(feed); } feed.file_url = feedlistCursor @@ -1557,8 +1556,8 @@ public class FeedManager { private void extractMediafromFeedItemlist(PodDBAdapter adapter, ArrayList<FeedItem> items, ArrayList<String> mediaIds) { ArrayList<FeedItem> itemsCopy = new ArrayList<FeedItem>(items); - Cursor cursor = adapter.getFeedMediaCursor(mediaIds - .toArray(new String[mediaIds.size()])); + Cursor cursor = adapter.getFeedMediaCursorByItemID(mediaIds + .toArray(new String[mediaIds.size()])); if (cursor.moveToFirst()) { do { long mediaId = cursor.getLong(PodDBAdapter.KEY_ID_INDEX); diff --git a/src/de/danoeh/antennapod/service/download/DownloadService.java b/src/de/danoeh/antennapod/service/download/DownloadService.java index 21a91f81f..b19b0482e 100644 --- a/src/de/danoeh/antennapod/service/download/DownloadService.java +++ b/src/de/danoeh/antennapod/service/download/DownloadService.java @@ -1,22 +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.*; +import java.util.concurrent.atomic.AtomicInteger; import javax.xml.parsers.ParserConfigurationException; -import de.danoeh.antennapod.storage.DBTasks; -import de.danoeh.antennapod.storage.DBWriter; +import de.danoeh.antennapod.storage.*; import org.xml.sax.SAXException; import android.annotation.SuppressLint; @@ -46,590 +41,622 @@ import de.danoeh.antennapod.feed.EventDistributor; import de.danoeh.antennapod.feed.Feed; 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 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() { + 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.equals(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); - t.setUncaughtExceptionHandler(new UncaughtExceptionHandler() { + 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 void uncaughtException(Thread thread, Throwable ex) { - Log.e(TAG, "Thread exited with uncaught exception"); - ex.printStackTrace(); - downloadsBeingHandled -= 1; - queryDownloads(); + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; } - }); - 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; + } + + @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) { + + + 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.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(); + ; } }); - 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(); - 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); - 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(); - 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.getDownloadRequest()); - 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"); - DownloadRequest 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; - } - - Downloader downloader = getDownloader(request); - if (downloader != null) { - downloads.add(downloader); - downloadExecutor.submit(downloader); - sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); - } - - queryDownloads(); - } - - private BroadcastReceiver downloadQueued = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - onDownloadQueued(intent); - } - - }; - - private Downloader getDownloader(DownloadRequest request) { - if (URLUtil.isHttpUrl(request.getSource())) { - return new HttpDownloader(new DownloaderCallback() { - - @Override - public void onDownloadCompleted(final Downloader downloader) { - handler.post(new Runnable() { - - @Override - public void run() { - DownloadService.this - .onDownloadCompleted(downloader); - } - }); - } - }, request); - } - Log.e(TAG, - "Could not find appropriate downloader for " - + request.getSource()); - 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.getResult(); - 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 { - 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.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(); - } - - /** 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(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 int 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 = 0; - 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; - handler.post(new Runnable() { - - @Override - public void run() { - try { - requester.downloadImage(DownloadService.this, - savedFeedRef.getImage()); - } catch (DownloadRequestException e) { - e.printStackTrace(); - DBWriter.addDownloadStatus( + } + + /** + * 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) { + 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 int 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 = 0; + 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; + handler.post(new Runnable() { + + @Override + public void run() { + try { + requester.downloadImage(DownloadService.this, + savedFeedRef.getImage()); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DBWriter.addDownloadStatus( DownloadService.this, new DownloadStatus( savedFeedRef.getImage(), @@ -638,205 +665,199 @@ public class DownloadService extends Service { .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(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 = FeedManager.getInstance().getFeedImage(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); - downloadsBeingHandled -= 1; - handler.post(new Runnable() { - - @Override - public void run() { - queryDownloads(); - - } - }); - } - } - - /** 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 = FeedManager.getInstance().getFeedMedia( - 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 = 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(); + } + } + }); + + } + + } 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.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(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 = 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(); try { if (chaptersRead) { @@ -849,61 +870,55 @@ public class DownloadService extends Service { e.printStackTrace(); } - if (!FeedManager.getInstance().isInQueue(media.getItem())) { - FeedManager.getInstance().addQueueItem(DownloadService.this, - media.getItem()); - } - - downloadsBeingHandled -= 1; - handler.post(new Runnable() { - - @Override - public void run() { - queryDownloads(); - - } - }); - } - } - - /** 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; - } + 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/Downloader.java b/src/de/danoeh/antennapod/service/download/Downloader.java index 8df2bf977..67507d40f 100644 --- a/src/de/danoeh/antennapod/service/download/Downloader.java +++ b/src/de/danoeh/antennapod/service/download/Downloader.java @@ -2,48 +2,36 @@ package de.danoeh.antennapod.service.download; import de.danoeh.antennapod.R; +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 DownloadRequest request; - protected volatile DownloadStatus result; + protected DownloadRequest request; + protected DownloadStatus result; - public Downloader(DownloaderCallback downloaderCallback, - DownloadRequest request) { + public Downloader(DownloadRequest request) { super(); - this.downloaderCallback = downloaderCallback; this.request = request; this.request.setStatusMsg(R.string.download_pending); this.cancelled = false; } - /** - * 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(); - @Override - public final void run() { + public final Downloader call() { download(); if (result == null) { throw new IllegalStateException( "Downloader hasn't created DownloadStatus object"); } - finish(); + finished = true; + return this; } public DownloadRequest getDownloadRequest() { diff --git a/src/de/danoeh/antennapod/service/download/HttpDownloader.java b/src/de/danoeh/antennapod/service/download/HttpDownloader.java index 77443956b..b533ca676 100644 --- a/src/de/danoeh/antennapod/service/download/HttpDownloader.java +++ b/src/de/danoeh/antennapod/service/download/HttpDownloader.java @@ -38,9 +38,8 @@ public class HttpDownloader extends Downloader { private static final int CONNECTION_TIMEOUT = 30000; private static final int SOCKET_TIMEOUT = 30000; - public HttpDownloader(DownloaderCallback downloaderCallback, - DownloadRequest request) { - super(downloaderCallback, request); + public HttpDownloader(DownloadRequest request) { + super(request); } private DefaultHttpClient createHttpClient() { @@ -144,7 +143,6 @@ public class HttpDownloader extends Downloader { e.printStackTrace(); onFail(DownloadError.ERROR_CONNECTION_ERROR, request.getSource()); } finally { - IOUtils.closeQuietly(connection); IOUtils.closeQuietly(out); if (httpClient != null) { httpClient.getConnectionManager().shutdown(); diff --git a/src/de/danoeh/antennapod/storage/DBReader.java b/src/de/danoeh/antennapod/storage/DBReader.java index c8135bea1..c69607473 100644 --- a/src/de/danoeh/antennapod/storage/DBReader.java +++ b/src/de/danoeh/antennapod/storage/DBReader.java @@ -188,41 +188,45 @@ public final class DBReader { List<FeedItem> items, ArrayList<String> itemIds) { List<FeedItem> itemsCopy = new ArrayList<FeedItem>(items); - Cursor cursor = adapter.getFeedMediaCursor(itemIds - .toArray(new String[itemIds.size()])); + Cursor cursor = adapter.getFeedMediaCursorByItemID(itemIds + .toArray(new String[itemIds.size()])); if (cursor.moveToFirst()) { do { - long mediaId = cursor.getLong(PodDBAdapter.KEY_ID_INDEX); long itemId = cursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX); // find matching feed item FeedItem item = getMatchingItemForMedia(itemId, itemsCopy); if (item != null) { - Date playbackCompletionDate = null; - long playbackCompletionTime = cursor - .getLong(PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE_INDEX); - if (playbackCompletionTime > 0) { - playbackCompletionDate = new Date( - playbackCompletionTime); - } - - item.setMedia(new FeedMedia( - mediaId, - item, - cursor.getInt(PodDBAdapter.KEY_DURATION_INDEX), - cursor.getInt(PodDBAdapter.KEY_POSITION_INDEX), - cursor.getLong(PodDBAdapter.KEY_SIZE_INDEX), - cursor.getString(PodDBAdapter.KEY_MIME_TYPE_INDEX), - cursor.getString(PodDBAdapter.KEY_FILE_URL_INDEX), - cursor.getString(PodDBAdapter.KEY_DOWNLOAD_URL_INDEX), - cursor.getInt(PodDBAdapter.KEY_DOWNLOADED_INDEX) > 0, - playbackCompletionDate)); - + item.setMedia(extractFeedMediaFromCursorRow(cursor)); + item.getMedia().setItem(item); } } while (cursor.moveToNext()); cursor.close(); } } + private static FeedMedia extractFeedMediaFromCursorRow(final Cursor cursor) { + long mediaId = cursor.getLong(PodDBAdapter.KEY_ID_INDEX); + Date playbackCompletionDate = null; + long playbackCompletionTime = cursor + .getLong(PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE_INDEX); + if (playbackCompletionTime > 0) { + playbackCompletionDate = new Date( + playbackCompletionTime); + } + + return new FeedMedia( + mediaId, + null, + cursor.getInt(PodDBAdapter.KEY_DURATION_INDEX), + cursor.getInt(PodDBAdapter.KEY_POSITION_INDEX), + cursor.getLong(PodDBAdapter.KEY_SIZE_INDEX), + cursor.getString(PodDBAdapter.KEY_MIME_TYPE_INDEX), + cursor.getString(PodDBAdapter.KEY_FILE_URL_INDEX), + cursor.getString(PodDBAdapter.KEY_DOWNLOAD_URL_INDEX), + cursor.getInt(PodDBAdapter.KEY_DOWNLOADED_INDEX) > 0, + playbackCompletionDate); + } + private static Feed extractFeedFromCursorRow(PodDBAdapter adapter, Cursor cursor) { Date lastUpdate = new Date( @@ -481,6 +485,21 @@ public final class DBReader { return result; } + /** + * Searches the DB for a FeedImage of the given id. + * + * @param imageId + * The id of the object + * @return The found object + * */ + public static FeedImage getFeedImage(final Context context, final long imageId) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + FeedImage result = getFeedImage(adapter, imageId); + adapter.close(); + return result; + } + /** * Searches the DB for a FeedImage of the given id. * @@ -488,7 +507,7 @@ public final class DBReader { * The id of the object * @return The found object * */ - public static FeedImage getFeedImage(PodDBAdapter adapter, final long id) { + static FeedImage getFeedImage(PodDBAdapter adapter, final long id) { Cursor cursor = adapter.getImageOfFeedCursor(id); if ((cursor.getCount() == 0) || !cursor.moveToFirst()) { throw new SQLException("No FeedImage found at index: " + id); @@ -504,4 +523,34 @@ public final class DBReader { cursor.close(); return image; } + + /** + * Searches the DB for a FeedMedia of the given id. + * + * @param mediaId + * The id of the object + * @return The found object + * */ + public static FeedMedia getFeedMedia(final Context context, final long mediaId) { + PodDBAdapter adapter = new PodDBAdapter(context); + + adapter.open(); + Cursor mediaCursor = adapter.getSingleFeedMediaCursor(mediaId); + + FeedMedia media = null; + if (mediaCursor.moveToFirst()) { + final long itemId = mediaCursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX); + media = extractFeedMediaFromCursorRow(mediaCursor); + FeedItem item = getFeedItem(context, itemId); + if (media != null && item != null) { + media.setItem(item); + item.setMedia(media); + } + } + + mediaCursor.close(); + adapter.close(); + + return media; + } } diff --git a/src/de/danoeh/antennapod/storage/DBTasks.java b/src/de/danoeh/antennapod/storage/DBTasks.java index 39c30445e..45ce4298a 100644 --- a/src/de/danoeh/antennapod/storage/DBTasks.java +++ b/src/de/danoeh/antennapod/storage/DBTasks.java @@ -7,6 +7,7 @@ import java.util.Comparator; import java.util.Date; import java.util.Iterator; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.concurrent.locks.ReentrantLock; import android.content.Context; @@ -25,6 +26,7 @@ import de.danoeh.antennapod.service.PlaybackService; import de.danoeh.antennapod.service.download.DownloadStatus; import de.danoeh.antennapod.util.DownloadError; import de.danoeh.antennapod.util.NetworkUtils; +import de.danoeh.antennapod.util.QueueAccess; import de.danoeh.antennapod.util.exception.MediaFileNotFoundException; public final class DBTasks { @@ -397,6 +399,11 @@ public final class DBTasks { return result; } + public static boolean isInQueue(Context context, final long feedItemId) { + List<Long> queue = DBReader.getQueueIDList(context); + return QueueAccess.IDListAccess(queue).contains(feedItemId); + } + private static Feed searchFeedByIdentifyingValue(Context context, String identifier) { List<Feed> feeds = DBReader.getFeedList(context); @@ -430,7 +437,13 @@ public final class DBTasks { "Found no existing Feed with title " + newFeed.getTitle() + ". Adding as new one."); // Add a new Feed - DBWriter.addNewFeed(context, newFeed); + try { + DBWriter.addNewFeed(context, newFeed).get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } return newFeed; } else { if (AppConfig.DEBUG) @@ -462,7 +475,13 @@ public final class DBTasks { // update attributes savedFeed.setLastUpdate(newFeed.getLastUpdate()); savedFeed.setType(newFeed.getType()); - DBWriter.setCompleteFeed(context, savedFeed); + try { + DBWriter.setCompleteFeed(context, savedFeed).get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } new Thread() { @Override public void run() { diff --git a/src/de/danoeh/antennapod/storage/DBWriter.java b/src/de/danoeh/antennapod/storage/DBWriter.java index 76f6306f8..a60694f35 100644 --- a/src/de/danoeh/antennapod/storage/DBWriter.java +++ b/src/de/danoeh/antennapod/storage/DBWriter.java @@ -488,8 +488,8 @@ public class DBWriter { } - static void addNewFeed(final Context context, final Feed feed) { - dbExec.submit(new Runnable() { + static Future<?> addNewFeed(final Context context, final Feed feed) { + return dbExec.submit(new Runnable() { @Override public void run() { @@ -503,8 +503,8 @@ public class DBWriter { }); } - static void setCompleteFeed(final Context context, final Feed feed) { - dbExec.submit(new Runnable() { + static Future<?> setCompleteFeed(final Context context, final Feed feed) { + return dbExec.submit(new Runnable() { @Override public void run() { diff --git a/src/de/danoeh/antennapod/storage/DownloadRequester.java b/src/de/danoeh/antennapod/storage/DownloadRequester.java index a27fbf8e4..4e74d5e98 100644 --- a/src/de/danoeh/antennapod/storage/DownloadRequester.java +++ b/src/de/danoeh/antennapod/storage/DownloadRequester.java @@ -24,273 +24,277 @@ import de.danoeh.antennapod.util.FileNameGenerator; import de.danoeh.antennapod.util.URLChecker; public class DownloadRequester { - private static final String TAG = "DownloadRequester"; - - public static String IMAGE_DOWNLOADPATH = "images/"; - public static String FEED_DOWNLOADPATH = "cache/"; - public static String MEDIA_DOWNLOADPATH = "media/"; - - private static DownloadRequester downloader; - - Map<String, DownloadRequest> downloads; - - private DownloadRequester() { - downloads = new ConcurrentHashMap<String, DownloadRequest>(); - } - - public static DownloadRequester getInstance() { - if (downloader == null) { - downloader = new DownloadRequester(); - } - return downloader; - } - - private void download(Context context, FeedFile item, File dest, - boolean overwriteIfExists) { - if (!isDownloadingFile(item)) { - if (!isFilenameAvailable(dest.toString()) || dest.exists()) { - if (AppConfig.DEBUG) - Log.d(TAG, "Filename already used."); - if (isFilenameAvailable(dest.toString()) && overwriteIfExists) { - boolean result = dest.delete(); - if (AppConfig.DEBUG) - Log.d(TAG, "Deleting file. Result: " + result); - } else { - // find different name - File newDest = null; - for (int i = 1; i < Integer.MAX_VALUE; i++) { - String newName = FilenameUtils.getBaseName(dest - .getName()) - + "-" - + i - + "." - + FilenameUtils.getExtension(dest.getName()); - if (AppConfig.DEBUG) - Log.d(TAG, "Testing filename " + newName); - newDest = new File(dest.getParent(), newName); - if (!newDest.exists() - && isFilenameAvailable(newDest.toString())) { - if (AppConfig.DEBUG) - Log.d(TAG, "File doesn't exist yet. Using " - + newName); - break; - } - } - if (newDest != null) { - dest = newDest; - } - } - } - if (AppConfig.DEBUG) - Log.d(TAG, - "Requesting download of url " + item.getDownload_url()); - item.setDownload_url(URLChecker.prepareURL(item.getDownload_url())); - - DownloadRequest request = new DownloadRequest(dest.toString(), - item.getDownload_url(), item.getHumanReadableIdentifier(), - item.getId(), item.getTypeAsInt()); - - downloads.put(request.getSource(), request); - - - 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); - } - EventDistributor.getInstance().sendDownloadQueuedBroadcast(); - } else { - Log.e(TAG, "URL " + item.getDownload_url() - + " is already being downloaded"); - } - } - - /** - * Returns true if a filename is available and false if it has already been - * taken by another requested download. - */ - private boolean isFilenameAvailable(String path) { - for (String key : downloads.keySet()) { - DownloadRequest r = downloads.get(key); - if (StringUtils.equals(r.getDestination(), path)) { - if (AppConfig.DEBUG) - Log.d(TAG, path - + " is already used by another requested download"); - return false; - } - } - if (AppConfig.DEBUG) - Log.d(TAG, path + " is available as a download destination"); - return true; - } - - public void downloadFeed(Context context, Feed feed) - throws DownloadRequestException { - if (feedFileValid(feed)) { - download(context, feed, new File(getFeedfilePath(context), - getFeedfileName(feed)), true); - } - } - - public void downloadImage(Context context, FeedImage image) - throws DownloadRequestException { - if (feedFileValid(image)) { - download(context, image, new File(getImagefilePath(context), - getImagefileName(image)), true); - } - } - - public void downloadMedia(Context context, FeedMedia feedmedia) - throws DownloadRequestException { - if (feedFileValid(feedmedia)) { - download(context, feedmedia, - new File(getMediafilePath(context, feedmedia), - getMediafilename(feedmedia)), false); - } - } - - /** - * Throws a DownloadRequestException if the feedfile or the download url of - * the feedfile is null. - * - * @throws DownloadRequestException - */ - private boolean feedFileValid(FeedFile f) throws DownloadRequestException { - if (f == null) { - throw new DownloadRequestException("Feedfile was null"); - } else if (f.getDownload_url() == null) { - throw new DownloadRequestException("File has no download URL"); - } else { - return true; - } - } - - /** - * Cancels a running download. - * */ - 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 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"); - 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 (DownloadRequest r : downloads.values()) { - if (r.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { - return true; - } - } - return false; - } - - /** Checks if feedfile is in the downloads list */ - public boolean isDownloadingFile(FeedFile item) { - if (item.getDownload_url() != null) { - return downloads.containsKey(item.getDownload_url()); - } - return false; - } - - public DownloadRequest 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) { - return downloads.get(downloadUrl) != null; - } - - public boolean hasNoDownloads() { - return downloads.isEmpty(); - } - - /** Remove an object from the downloads-list of the requester. */ - public void removeDownload(DownloadRequest r) { - if (downloads.remove(r.getSource()) == null) { - Log.e(TAG, - "Could not remove object with url " + r.getSource()); - } - } - - /** Get the number of uncompleted Downloads */ - public int getNumberOfDownloads() { - return downloads.size(); - } - - public String getFeedfilePath(Context context) - throws DownloadRequestException { - return getExternalFilesDirOrThrowException(context, FEED_DOWNLOADPATH) - .toString() + "/"; - } - - public String getFeedfileName(Feed feed) { - String filename = feed.getDownload_url(); - if (feed.getTitle() != null && !feed.getTitle().isEmpty()) { - filename = feed.getTitle(); - } - return "feed-" + FileNameGenerator.generateFileName(filename); - } - - public String getImagefilePath(Context context) - throws DownloadRequestException { - return getExternalFilesDirOrThrowException(context, IMAGE_DOWNLOADPATH) - .toString() + "/"; - } - - public String getImagefileName(FeedImage image) { - String filename = image.getDownload_url(); - if (image.getFeed() != null && image.getFeed().getTitle() != null) { - filename = image.getFeed().getTitle(); - } - return "image-" + FileNameGenerator.generateFileName(filename); - } - - public String getMediafilePath(Context context, FeedMedia media) - throws DownloadRequestException { - File externalStorage = getExternalFilesDirOrThrowException( - context, - MEDIA_DOWNLOADPATH - + FileNameGenerator.generateFileName(media.getItem() - .getFeed().getTitle()) + "/"); - return externalStorage.toString(); - } - - private File getExternalFilesDirOrThrowException(Context context, - String type) throws DownloadRequestException { - File result = UserPreferences.getDataFolder(context, type); - if (result == null) { - throw new DownloadRequestException( - "Failed to access external storage"); - } - return result; - } - - public String getMediafilename(FeedMedia media) { - return URLUtil.guessFileName(media.getDownload_url(), null, - media.getMime_type()); - } + private static final String TAG = "DownloadRequester"; + + public static String IMAGE_DOWNLOADPATH = "images/"; + public static String FEED_DOWNLOADPATH = "cache/"; + public static String MEDIA_DOWNLOADPATH = "media/"; + + private static DownloadRequester downloader; + + Map<String, DownloadRequest> downloads; + + private DownloadRequester() { + downloads = new ConcurrentHashMap<String, DownloadRequest>(); + } + + public static DownloadRequester getInstance() { + if (downloader == null) { + downloader = new DownloadRequester(); + } + return downloader; + } + + private void download(Context context, FeedFile item, File dest, + boolean overwriteIfExists) { + if (!isDownloadingFile(item)) { + if (!isFilenameAvailable(dest.toString()) || dest.exists()) { + if (AppConfig.DEBUG) + Log.d(TAG, "Filename already used."); + if (isFilenameAvailable(dest.toString()) && overwriteIfExists) { + boolean result = dest.delete(); + if (AppConfig.DEBUG) + Log.d(TAG, "Deleting file. Result: " + result); + } else { + // find different name + File newDest = null; + for (int i = 1; i < Integer.MAX_VALUE; i++) { + String newName = FilenameUtils.getBaseName(dest + .getName()) + + "-" + + i + + "." + + FilenameUtils.getExtension(dest.getName()); + if (AppConfig.DEBUG) + Log.d(TAG, "Testing filename " + newName); + newDest = new File(dest.getParent(), newName); + if (!newDest.exists() + && isFilenameAvailable(newDest.toString())) { + if (AppConfig.DEBUG) + Log.d(TAG, "File doesn't exist yet. Using " + + newName); + break; + } + } + if (newDest != null) { + dest = newDest; + } + } + } + if (AppConfig.DEBUG) + Log.d(TAG, + "Requesting download of url " + item.getDownload_url()); + item.setDownload_url(URLChecker.prepareURL(item.getDownload_url())); + + DownloadRequest request = new DownloadRequest(dest.toString(), + item.getDownload_url(), item.getHumanReadableIdentifier(), + item.getId(), item.getTypeAsInt()); + + downloads.put(request.getSource(), request); + + Intent launchIntent = new Intent(context, DownloadService.class); + launchIntent.putExtra(DownloadService.EXTRA_REQUEST, request); + context.startService(launchIntent); + EventDistributor.getInstance().sendDownloadQueuedBroadcast(); + } else { + Log.e(TAG, "URL " + item.getDownload_url() + + " is already being downloaded"); + } + } + + /** + * Returns true if a filename is available and false if it has already been + * taken by another requested download. + */ + private boolean isFilenameAvailable(String path) { + for (String key : downloads.keySet()) { + DownloadRequest r = downloads.get(key); + if (StringUtils.equals(r.getDestination(), path)) { + if (AppConfig.DEBUG) + Log.d(TAG, path + + " is already used by another requested download"); + return false; + } + } + if (AppConfig.DEBUG) + Log.d(TAG, path + " is available as a download destination"); + return true; + } + + public void downloadFeed(Context context, Feed feed) + throws DownloadRequestException { + if (feedFileValid(feed)) { + download(context, feed, new File(getFeedfilePath(context), + getFeedfileName(feed)), true); + } + } + + public void downloadImage(Context context, FeedImage image) + throws DownloadRequestException { + if (feedFileValid(image)) { + download(context, image, new File(getImagefilePath(context), + getImagefileName(image)), true); + } + } + + public void downloadMedia(Context context, FeedMedia feedmedia) + throws DownloadRequestException { + if (feedFileValid(feedmedia)) { + download(context, feedmedia, + new File(getMediafilePath(context, feedmedia), + getMediafilename(feedmedia)), false); + } + } + + /** + * Throws a DownloadRequestException if the feedfile or the download url of + * the feedfile is null. + * + * @throws DownloadRequestException + */ + private boolean feedFileValid(FeedFile f) throws DownloadRequestException { + if (f == null) { + throw new DownloadRequestException("Feedfile was null"); + } else if (f.getDownload_url() == null) { + throw new DownloadRequestException("File has no download URL"); + } else { + return true; + } + } + + /** + * Cancels a running download. + */ + 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 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"); + 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 (DownloadRequest r : downloads.values()) { + if (r.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { + return true; + } + } + return false; + } + + /** + * Checks if feedfile is in the downloads list + */ + public boolean isDownloadingFile(FeedFile item) { + if (item.getDownload_url() != null) { + return downloads.containsKey(item.getDownload_url()); + } + return false; + } + + public DownloadRequest 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) { + return downloads.get(downloadUrl) != null; + } + + public boolean hasNoDownloads() { + return downloads.isEmpty(); + } + + /** + * Remove an object from the downloads-list of the requester. + */ + public void removeDownload(DownloadRequest r) { + if (downloads.remove(r.getSource()) == null) { + Log.e(TAG, + "Could not remove object with url " + r.getSource()); + } + } + + /** + * Get the number of uncompleted Downloads + */ + public int getNumberOfDownloads() { + return downloads.size(); + } + + public String getFeedfilePath(Context context) + throws DownloadRequestException { + return getExternalFilesDirOrThrowException(context, FEED_DOWNLOADPATH) + .toString() + "/"; + } + + public String getFeedfileName(Feed feed) { + String filename = feed.getDownload_url(); + if (feed.getTitle() != null && !feed.getTitle().isEmpty()) { + filename = feed.getTitle(); + } + return "feed-" + FileNameGenerator.generateFileName(filename); + } + + public String getImagefilePath(Context context) + throws DownloadRequestException { + return getExternalFilesDirOrThrowException(context, IMAGE_DOWNLOADPATH) + .toString() + "/"; + } + + public String getImagefileName(FeedImage image) { + String filename = image.getDownload_url(); + if (image.getFeed() != null && image.getFeed().getTitle() != null) { + filename = image.getFeed().getTitle(); + } + return "image-" + FileNameGenerator.generateFileName(filename); + } + + public String getMediafilePath(Context context, FeedMedia media) + throws DownloadRequestException { + File externalStorage = getExternalFilesDirOrThrowException( + context, + MEDIA_DOWNLOADPATH + + FileNameGenerator.generateFileName(media.getItem() + .getFeed().getTitle()) + "/"); + return externalStorage.toString(); + } + + private File getExternalFilesDirOrThrowException(Context context, + String type) throws DownloadRequestException { + File result = UserPreferences.getDataFolder(context, type); + if (result == null) { + throw new DownloadRequestException( + "Failed to access external storage"); + } + return result; + } + + public String getMediafilename(FeedMedia media) { + return URLUtil.guessFileName(media.getDownload_url(), null, + media.getMime_type()); + } } diff --git a/src/de/danoeh/antennapod/storage/PodDBAdapter.java b/src/de/danoeh/antennapod/storage/PodDBAdapter.java index 1aa8c93d4..14ef07c34 100644 --- a/src/de/danoeh/antennapod/storage/PodDBAdapter.java +++ b/src/de/danoeh/antennapod/storage/PodDBAdapter.java @@ -201,6 +201,13 @@ public class PodDBAdapter { TABLE_NAME_FEED_ITEMS + "." + KEY_HAS_CHAPTERS, TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER }; + /** Contains SEL_FI_SMALL as comma-separated list. Useful for raw queries. */ + private static final String SEL_FI_SMALL_STR; + static { + String selFiSmall = Arrays.toString(SEL_FI_SMALL); + SEL_FI_SMALL_STR = selFiSmall.substring(1, selFiSmall.length() - 1); + } + // column indices for SEL_FI_SMALL public static final int IDX_FI_SMALL_ID = 0; @@ -601,7 +608,7 @@ public class PodDBAdapter { public final Cursor getAllFeedsCursor() { open(); Cursor c = db.query(TABLE_NAME_FEEDS, null, null, null, null, null, - null); + KEY_TITLE + " ASC"); return c; } @@ -692,9 +699,8 @@ public class PodDBAdapter { */ public final Cursor getQueueCursor() { open(); - String selFiSmall = Arrays.toString(SEL_FI_SMALL); Object[] args = (Object[]) new String[] { - selFiSmall.substring(1, selFiSmall.length() - 1) + "," + TABLE_NAME_QUEUE + "." + KEY_ID, + SEL_FI_SMALL_STR + "," + TABLE_NAME_QUEUE + "." + KEY_ID, TABLE_NAME_FEED_ITEMS, TABLE_NAME_QUEUE, TABLE_NAME_FEED_ITEMS + "." + KEY_ID, TABLE_NAME_QUEUE + "." + KEY_FEEDITEM, @@ -738,12 +744,12 @@ public class PodDBAdapter { public Cursor getDownloadedItemsCursor() { open(); - Cursor c = db.rawQuery("SELECT ? FROM " + TABLE_NAME_FEED_ITEMS - + "FULL JOIN " + TABLE_NAME_FEED_MEDIA + " ON " - + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" - + TABLE_NAME_FEED_MEDIA + "." + KEY_ID + " WHERE " - + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + ">0", - SEL_FI_SMALL); + final String query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + + " INNER JOIN " + TABLE_NAME_FEED_MEDIA + " ON " + + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + + TABLE_NAME_FEED_MEDIA + "." + KEY_ID + " WHERE " + + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + ">0"; + Cursor c = db.rawQuery(query, null); return c; } @@ -768,7 +774,11 @@ public class PodDBAdapter { return c; } - public final Cursor getFeedMediaCursor(String... mediaIds) { + public final Cursor getSingleFeedMediaCursor(long id) { + return db.query(TABLE_NAME_FEED_MEDIA, null, KEY_ID + "=?", new String[] {Long.toString(id)}, null, null, null); + } + + public final Cursor getFeedMediaCursorByItemID(String... mediaIds) { int length = mediaIds.length; if (length > IN_OPERATOR_MAXIMUM) { Log.w(TAG, "Length of id array is larger than " @@ -837,11 +847,14 @@ public class PodDBAdapter { } public final int getNumberOfDownloadedEpisodes() { + final String query = "SELECT COUNT(DISTINCT " + KEY_ID + ") AS count FROM " + TABLE_NAME_FEED_MEDIA + + " WHERE " + KEY_DOWNLOADED + " > 0"; - Cursor c = db.rawQuery( - "SELECT COUNT(DISTINCT ?) AS count FROM ? WHERE ?>0", - new String[] { KEY_ID, TABLE_NAME_FEED_MEDIA, KEY_DOWNLOADED }); - final int result = c.getInt(0); + Cursor c = db.rawQuery(query, null); + int result = 0; + if (c.moveToFirst()) { + result = c.getInt(0); + } c.close(); return result; } |