diff options
Diffstat (limited to 'src/de/danoeh/antennapod/service/download/DownloadService.java')
-rw-r--r-- | src/de/danoeh/antennapod/service/download/DownloadService.java | 1230 |
1 files changed, 0 insertions, 1230 deletions
diff --git a/src/de/danoeh/antennapod/service/download/DownloadService.java b/src/de/danoeh/antennapod/service/download/DownloadService.java deleted file mode 100644 index 63be91b57..000000000 --- a/src/de/danoeh/antennapod/service/download/DownloadService.java +++ /dev/null @@ -1,1230 +0,0 @@ -package de.danoeh.antennapod.service.download; - -import android.annotation.SuppressLint; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.media.MediaMetadataRetriever; -import android.os.Binder; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.support.v4.app.NotificationCompat; -import android.util.Log; -import android.webkit.URLUtil; - -import org.apache.commons.io.FileUtils; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; -import org.apache.http.HttpStatus; -import org.xml.sax.SAXException; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.LinkedList; -import java.util.List; -import java.util.Queue; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.Callable; -import java.util.concurrent.CompletionService; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorCompletionService; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import javax.xml.parsers.ParserConfigurationException; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.DownloadAuthenticationActivity; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.NavListAdapter; -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.FeedMedia; -import de.danoeh.antennapod.feed.FeedPreferences; -import de.danoeh.antennapod.fragment.DownloadsFragment; -import de.danoeh.antennapod.storage.DBReader; -import de.danoeh.antennapod.storage.DBTasks; -import de.danoeh.antennapod.storage.DBWriter; -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"; - - /** - * 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 new media files that will be queued for auto-download if possible. - */ - private List<Long> newMediaFiles; - - /** - * Contains all completed downloads that have not been included in the report yet. - */ - private List<DownloadStatus> reportQueue; - - private ExecutorService syncExecutor; - private CompletionService<Downloader> downloadExecutor; - private FeedSyncThread feedSyncThread; - - /** - * Number of threads of downloadExecutor. - */ - private static final int NUM_PARALLEL_DOWNLOADS = 6; - - 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 (BuildConfig.DEBUG) Log.d(TAG, "downloadCompletionThread was started"); - while (!isInterrupted()) { - try { - Downloader downloader = downloadExecutor.take().get(); - if (BuildConfig.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 (!status.isCancelled()) { - if (status.getReason() == DownloadError.ERROR_UNAUTHORIZED) { - postAuthenticationNotification(downloader.getDownloadRequest()); - } else if (status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR - && Integer.valueOf(status.getReasonDetailed()) == HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE) { - - Log.d(TAG, "Requested invalid range, restarting download from the beginning"); - FileUtils.deleteQuietly(new File(downloader.getDownloadRequest().getDestination())); - DownloadRequester.getInstance().download(DownloadService.this, downloader.getDownloadRequest()); - } else { - Log.e(TAG, "Download failed"); - saveDownloadStatus(status); - handleFailedDownload(status, downloader.getDownloadRequest()); - } - } - sendDownloadHandledIntent(); - queryDownloadsAsync(); - } - } catch (InterruptedException e) { - if (BuildConfig.DEBUG) Log.d(TAG, "DownloadCompletionThread was interrupted"); - } catch (ExecutionException e) { - e.printStackTrace(); - numberOfDownloads.decrementAndGet(); - } - } - if (BuildConfig.DEBUG) Log.d(TAG, "End of downloadCompletionThread"); - } - }; - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (intent.getParcelableExtra(EXTRA_REQUEST) != null) { - onDownloadQueued(intent); - } else if (numberOfDownloads.get() == 0) { - stopSelf(); - } - return Service.START_NOT_STICKY; - } - - @SuppressLint("NewApi") - @Override - public void onCreate() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Service started"); - isRunning = true; - handler = new Handler(); - newMediaFiles = Collections.synchronizedList(new ArrayList<Long>()); - reportQueue = Collections.synchronizedList(new ArrayList<DownloadStatus>()); - downloads = new ArrayList<Downloader>(); - numberOfDownloads = new AtomicInteger(0); - - IntentFilter cancelDownloadReceiverFilter = new IntentFilter(); - cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_ALL_DOWNLOADS); - cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_DOWNLOAD); - registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter); - syncExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() { - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }); - downloadExecutor = new ExecutorCompletionService<Downloader>( - Executors.newFixedThreadPool(NUM_PARALLEL_DOWNLOADS, - new ThreadFactory() { - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - } - ) - ); - schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, - new ThreadFactory() { - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }, new RejectedExecutionHandler() { - - @Override - public void rejectedExecution(Runnable r, - ThreadPoolExecutor executor) { - Log.w(TAG, "SchedEx rejected submission of new task"); - } - } - ); - downloadCompletionThread.start(); - feedSyncThread = new FeedSyncThread(); - feedSyncThread.start(); - - setupNotificationBuilders(); - requester = DownloadRequester.getInstance(); - } - - @Override - public IBinder onBind(Intent intent) { - return mBinder; - } - - @Override - public void onDestroy() { - if (BuildConfig.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(); - feedSyncThread.shutdown(); - cancelNotificationUpdater(); - unregisterReceiver(cancelDownloadReceiver); - - if (!newMediaFiles.isEmpty()) { - DBTasks.autodownloadUndownloadedItems(getApplicationContext(), - ArrayUtils.toPrimitive(newMediaFiles.toArray(new Long[newMediaFiles.size()]))); - } - } - - @SuppressLint("NewApi") - private void setupNotificationBuilders() { - Intent intent = new Intent(this, MainActivity.class); - intent.putExtra(MainActivity.EXTRA_NAV_TYPE, NavListAdapter.VIEW_TYPE_NAV); - intent.putExtra(MainActivity.EXTRA_NAV_INDEX, MainActivity.POS_DOWNLOADS); - Bundle args = new Bundle(); - args.putInt(DownloadsFragment.ARG_SELECTED_TAB, DownloadsFragment.POS_RUNNING); - intent.putExtra(MainActivity.EXTRA_FRAGMENT_ARGS, args); - - PendingIntent pIntent = PendingIntent.getActivity(this, 0, intent, - 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 (BuildConfig.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); - int numDownloads = requester.getNumberOfDownloads(); - String downloadsLeft; - if (numDownloads > 0) { - downloadsLeft = requester.getNumberOfDownloads() - + getString(R.string.downloads_left); - } else { - downloadsLeft = getString(R.string.downloads_processing); - } - 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.build(); - } - } - 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 (StringUtils.equals(intent.getAction(), ACTION_CANCEL_DOWNLOAD)) { - String url = intent.getStringExtra(EXTRA_DOWNLOAD_URL); - Validate.notNull(url, "ACTION_CANCEL_DOWNLOAD intent needs download url extra"); - - if (BuildConfig.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 (StringUtils.equals(intent.getAction(), ACTION_CANCEL_ALL_DOWNLOADS)) { - for (Downloader d : downloads) { - d.cancel(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Cancelled all downloads"); - } - sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); - - } - queryDownloads(); - } - - }; - - private void onDownloadQueued(Intent intent) { - if (BuildConfig.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()) - || URLUtil.isHttpsUrl(request.getSource())) { - return new HttpDownloader(request); - } - Log.e(TAG, - "Could not find appropriate downloader for " - + request.getSource() - ); - return null; - } - - /** - * Remove download from the DownloadRequester list and from the - * DownloadService list. - */ - private void removeDownload(final Downloader d) { - handler.post(new Runnable() { - @Override - public void run() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Removing downloader: " - + d.getDownloadRequest().getSource()); - boolean rc = downloads.remove(d); - if (BuildConfig.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) { - reportQueue.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 : reportQueue) { - if (status.isSuccessful()) { - successfulDownloads++; - } else if (!status.isCancelled()) { - if (status.getFeedfileType() != FeedImage.FEEDFILETYPE_FEEDIMAGE) { - createReport = true; - } - failedDownloads++; - } - } - - if (createReport) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Creating report"); - Intent intent = new Intent(this, MainActivity.class); - intent.putExtra(MainActivity.EXTRA_NAV_TYPE, NavListAdapter.VIEW_TYPE_NAV); - intent.putExtra(MainActivity.EXTRA_NAV_INDEX, MainActivity.POS_DOWNLOADS); - Bundle args = new Bundle(); - args.putInt(DownloadsFragment.ARG_SELECTED_TAB, DownloadsFragment.POS_LOG); - intent.putExtra(MainActivity.EXTRA_FRAGMENT_ARGS, args); - - // 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, intent, PendingIntent.FLAG_UPDATE_CURRENT) - ) - .setAutoCancel(true).build(); - NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(REPORT_ID, notification); - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "No report is created"); - } - reportQueue.clear(); - } - - /** - * Calls query downloads on the services main thread. This method should be used instead of queryDownloads if it is - * used from a thread other than the main thread. - */ - void queryDownloadsAsync() { - handler.post(new Runnable() { - public void run() { - queryDownloads(); - ; - } - }); - } - - /** - * Check if there's something else to download, otherwise stop - */ - void queryDownloads() { - if (BuildConfig.DEBUG) { - Log.d(TAG, numberOfDownloads.get() + " downloads left"); - } - - if (numberOfDownloads.get() <= 0 && DownloadRequester.getInstance().hasNoDownloads()) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Number of downloads is " + numberOfDownloads.get() + ", attempting shutdown"); - stopSelf(); - } else { - setupNotificationUpdater(); - startForeground(NOTIFICATION_ID, updateNotifications()); - } - } - - private void postAuthenticationNotification(final DownloadRequest downloadRequest) { - handler.post(new Runnable() { - @Override - public void run() { - final String resourceTitle = (downloadRequest.getTitle() != null) - ? downloadRequest.getTitle() : downloadRequest.getSource(); - - final Intent activityIntent = new Intent(getApplicationContext(), DownloadAuthenticationActivity.class); - activityIntent.putExtra(DownloadAuthenticationActivity.ARG_DOWNLOAD_REQUEST, downloadRequest); - activityIntent.putExtra(DownloadAuthenticationActivity.ARG_SEND_TO_DOWNLOAD_REQUESTER_BOOL, true); - final PendingIntent contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, activityIntent, PendingIntent.FLAG_ONE_SHOT); - - NotificationCompat.Builder builder = new NotificationCompat.Builder(DownloadService.this); - builder.setTicker(getText(R.string.authentication_notification_title)) - .setContentTitle(getText(R.string.authentication_notification_title)) - .setContentText(getText(R.string.authentication_notification_msg)) - .setStyle(new NotificationCompat.BigTextStyle().bigText(getText(R.string.authentication_notification_msg) - + ": " + resourceTitle)) - .setSmallIcon(R.drawable.ic_stat_authentication) - .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_stat_authentication)) - .setAutoCancel(true) - .setContentIntent(contentIntent); - Notification n = builder.build(); - NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(downloadRequest.getSource().hashCode(), n); - } - }); - } - - /** - * Is called whenever a Feed is downloaded - */ - private void handleCompletedFeedDownload(DownloadRequest request) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Handling completed Feed Download"); - feedSyncThread.submitCompletedDownload(request); - - } - - /** - * Is called whenever a Feed-Image is downloaded - */ - private void handleCompletedImageDownload(DownloadStatus status, DownloadRequest request) { - if (BuildConfig.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 (BuildConfig.DEBUG) - Log.d(TAG, "Handling completed FeedMedia Download"); - syncExecutor.execute(new MediaHandlerThread(status, request)); - } - - private void handleFailedDownload(DownloadStatus status, DownloadRequest request) { - if (BuildConfig.DEBUG) Log.d(TAG, "Handling failed download"); - syncExecutor.execute(new FailedDownloadHandler(status, request)); - } - - /** - * Takes a single Feed, parses the corresponding file and refreshes - * information in the manager - */ - class FeedSyncThread extends Thread { - private static final String TAG = "FeedSyncThread"; - - private BlockingQueue<DownloadRequest> completedRequests = new LinkedBlockingDeque<DownloadRequest>(); - private CompletionService<Feed> parserService = new ExecutorCompletionService<Feed>(Executors.newSingleThreadExecutor()); - private ExecutorService dbService = Executors.newSingleThreadExecutor(); - private Future<?> dbUpdateFuture; - private volatile boolean isActive = true; - private volatile boolean isCollectingRequests = false; - - private final long WAIT_TIMEOUT = 3000; - - - /** - * Waits for completed requests. Once the first request has been taken, the method will wait WAIT_TIMEOUT ms longer to - * collect more completed requests. - * - * @return Collected feeds or null if the method has been interrupted during the first waiting period. - */ - private List<Feed> collectCompletedRequests() { - List<Feed> results = new LinkedList<Feed>(); - DownloadRequester requester = DownloadRequester.getInstance(); - int tasks = 0; - - try { - DownloadRequest request = completedRequests.take(); - parserService.submit(new FeedParserTask(request)); - tasks++; - } catch (InterruptedException e) { - return null; - } - - tasks += pollCompletedDownloads(); - - isCollectingRequests = true; - - if (requester.isDownloadingFeeds()) { - // wait for completion of more downloads - long startTime = System.currentTimeMillis(); - long currentTime = startTime; - while (requester.isDownloadingFeeds() && (currentTime - startTime) < WAIT_TIMEOUT) { - try { - if (BuildConfig.DEBUG) - Log.d(TAG, "Waiting for " + (startTime + WAIT_TIMEOUT - currentTime) + " ms"); - sleep(startTime + WAIT_TIMEOUT - currentTime); - } catch (InterruptedException e) { - if (BuildConfig.DEBUG) - Log.d(TAG, "interrupted while waiting for more downloads"); - tasks += pollCompletedDownloads(); - } finally { - currentTime = System.currentTimeMillis(); - } - } - - tasks += pollCompletedDownloads(); - - } - - isCollectingRequests = false; - - for (int i = 0; i < tasks; i++) { - try { - Feed f = parserService.take().get(); - if (f != null) { - results.add(f); - } - } catch (InterruptedException e) { - e.printStackTrace(); - - } catch (ExecutionException e) { - e.printStackTrace(); - } - } - - return results; - } - - private int pollCompletedDownloads() { - int tasks = 0; - for (int i = 0; i < completedRequests.size(); i++) { - parserService.submit(new FeedParserTask(completedRequests.poll())); - tasks++; - } - return tasks; - } - - @Override - public void run() { - while (isActive) { - final List<Feed> feeds = collectCompletedRequests(); - - if (feeds == null) { - continue; - } - - if (BuildConfig.DEBUG) Log.d(TAG, "Bundling " + feeds.size() + " feeds"); - - for (Feed feed : feeds) { - removeDuplicateImages(feed); // duplicate images have to removed because the DownloadRequester does not accept two downloads with the same download URL yet. - } - - // Save information of feed in DB - if (dbUpdateFuture != null) { - try { - dbUpdateFuture.get(); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - } - - dbUpdateFuture = dbService.submit(new Runnable() { - @Override - public void run() { - Feed[] savedFeeds = DBTasks.updateFeed(DownloadService.this, feeds.toArray(new Feed[feeds.size()])); - - for (Feed savedFeed : savedFeeds) { - // Download Feed Image if provided and not downloaded - if (savedFeed.getImage() != null - && savedFeed.getImage().isDownloaded() == false) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Feed has image; Downloading...."); - savedFeed.getImage().setOwner(savedFeed); - final Feed savedFeedRef = savedFeed; - try { - requester.downloadImage(DownloadService.this, - savedFeedRef.getImage()); - } catch (DownloadRequestException e) { - e.printStackTrace(); - DBWriter.addDownloadStatus( - DownloadService.this, - new DownloadStatus( - savedFeedRef.getImage(), - savedFeedRef - .getImage() - .getHumanReadableIdentifier(), - DownloadError.ERROR_REQUEST_ERROR, - false, e.getMessage() - ) - ); - } - } - - // queue new media files for automatic download - for (FeedItem item : savedFeed.getItems()) { - if (!item.isRead() && item.hasMedia() && !item.getMedia().isDownloaded()) { - newMediaFiles.add(item.getMedia().getId()); - } - } - - numberOfDownloads.decrementAndGet(); - } - - sendDownloadHandledIntent(); - - queryDownloadsAsync(); - } - }); - - } - - if (dbUpdateFuture != null) { - try { - dbUpdateFuture.get(); - } catch (InterruptedException e) { - } catch (ExecutionException e) { - e.printStackTrace(); - } - } - - if (BuildConfig.DEBUG) Log.d(TAG, "Shutting down"); - - } - - private class FeedParserTask implements Callable<Feed> { - - private DownloadRequest request; - - private FeedParserTask(DownloadRequest request) { - this.request = request; - } - - @Override - public Feed call() throws Exception { - return parseFeed(request); - } - } - - private Feed parseFeed(DownloadRequest request) { - Feed savedFeed = null; - - Feed feed = new Feed(request.getSource(), new Date()); - feed.setFile_url(request.getDestination()); - feed.setId(request.getFeedfileId()); - feed.setDownloaded(true); - feed.setPreferences(new FeedPreferences(0, true, request.getUsername(), request.getPassword())); - - DownloadError reason = null; - String reasonDetailed = null; - boolean successful = true; - FeedHandler feedHandler = new FeedHandler(); - - try { - feed = feedHandler.parseFeed(feed).feed; - if (BuildConfig.DEBUG) - Log.d(TAG, feed.getTitle() + " parsed"); - if (checkFeedData(feed) == false) { - throw new InvalidFeedException(); - } - - } 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; - } - - - if (successful) { - return savedFeed; - } else { - numberOfDownloads.decrementAndGet(); - saveDownloadStatus(new DownloadStatus(savedFeed, - savedFeed.getHumanReadableIdentifier(), reason, successful, - reasonDetailed)); - return null; - } - } - - - /** - * 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; - } - return true; - } - - /** - * Checks if the FeedItems of this feed have images that point - * to the same URL. If two FeedItems have an image that points to - * the same URL, the reference of the second item is removed, so that every image - * reference is unique. - */ - private void removeDuplicateImages(Feed feed) { - for (int x = 0; x < feed.getItems().size(); x++) { - for (int y = x + 1; y < feed.getItems().size(); y++) { - FeedItem item1 = feed.getItems().get(x); - FeedItem item2 = feed.getItems().get(y); - if (item1.hasItemImage() && item2.hasItemImage()) { - if (StringUtils.equals(item1.getImage().getDownload_url(), item2.getImage().getDownload_url())) { - item2.setImage(null); - } - } - } - } - } - - private boolean hasValidFeedItems(Feed feed) { - for (FeedItem item : feed.getItems()) { - if (item.getTitle() == null) { - Log.e(TAG, "Item has no title"); - return false; - } - if (item.getPubDate() == null) { - Log.e(TAG, - "Item has no pubDate. Using current time as pubDate"); - if (item.getTitle() != null) { - Log.e(TAG, "Title of invalid item: " + item.getTitle()); - } - item.setPubDate(new Date()); - } - } - return true; - } - - /** - * Delete files that aren't needed anymore - */ - private void cleanup(Feed feed) { - if (feed.getFile_url() != null) { - if (new File(feed.getFile_url()).delete()) - if (BuildConfig.DEBUG) - Log.d(TAG, "Successfully deleted cache file."); - else - Log.e(TAG, "Failed to delete cache file."); - feed.setFile_url(null); - } else if (BuildConfig.DEBUG) { - Log.d(TAG, "Didn't delete cache file: File url is not set."); - } - } - - public void shutdown() { - isActive = false; - if (isCollectingRequests) { - interrupt(); - } - } - - public void submitCompletedDownload(DownloadRequest request) { - completedRequests.offer(request); - if (isCollectingRequests) { - interrupt(); - } - } - - } - - /** - * Handles failed downloads. - * <p/> - * If the file has been partially downloaded, this handler will set the file_url of the FeedFile to the location - * of the downloaded file. - * <p/> - * Currently, this handler only handles FeedMedia objects, because Feeds and FeedImages are deleted if the download fails. - */ - class FailedDownloadHandler implements Runnable { - - private DownloadRequest request; - private DownloadStatus status; - - FailedDownloadHandler(DownloadStatus status, DownloadRequest request) { - this.request = request; - this.status = status; - } - - @Override - public void run() { - if (request.isDeleteOnFailure()) { - if (BuildConfig.DEBUG) Log.d(TAG, "Ignoring failed download, deleteOnFailure=true"); - } else { - File dest = new File(request.getDestination()); - if (dest.exists() && request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - Log.d(TAG, "File has been partially downloaded. Writing file url"); - FeedMedia media = DBReader.getFeedMedia(DownloadService.this, request.getFeedfileId()); - media.setFile_url(request.getDestination()); - try { - DBWriter.setFeedMedia(DownloadService.this, media).get(); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - } - } - } - } - - /** - * Handles a completed image download. - */ - class ImageHandlerThread implements Runnable { - - private DownloadRequest request; - private DownloadStatus status; - - public ImageHandlerThread(DownloadStatus status, DownloadRequest request) { - Validate.notNull(status); - Validate.notNull(request); - - 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) { - Validate.notNull(status); - Validate.notNull(request); - - 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 - MediaMetadataRetriever mmr = null; - try { - mmr = new MediaMetadataRetriever(); - mmr.setDataSource(media.getFile_url()); - String durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); - media.setDuration(Integer.parseInt(durationStr)); - if (BuildConfig.DEBUG) - Log.d(TAG, "Duration of file is " + media.getDuration()); - } catch (NumberFormatException e) { - e.printStackTrace(); - } catch (RuntimeException e) { - e.printStackTrace(); - } finally { - if (mmr != null) { - mmr.release(); - } - } - - if (media.getItem().getChapters() == null) { - ChapterUtils.loadChaptersFromFileUrl(media); - if (media.getItem().getChapters() != null) { - chaptersRead = true; - } - } - - try { - if (chaptersRead) { - DBWriter.setFeedItem(DownloadService.this, media.getItem()).get(); - } - DBWriter.setFeedMedia(DownloadService.this, media).get(); - if (!DBTasks.isInQueue(DownloadService.this, media.getItem().getId())) { - DBWriter.addQueueItem(DownloadService.this, media.getItem().getId()).get(); - } - } catch (ExecutionException e) { - e.printStackTrace(); - status = new DownloadStatus(media, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.getMessage()); - } catch (InterruptedException e) { - e.printStackTrace(); - status = new DownloadStatus(media, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.getMessage()); - } - - saveDownloadStatus(status); - sendDownloadHandledIntent(); - - numberOfDownloads.decrementAndGet(); - queryDownloadsAsync(); - } - } - - /** - * Schedules the notification updater task if it hasn't been scheduled yet. - */ - private void setupNotificationUpdater() { - if (BuildConfig.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; - } - -} |