/* ********************************************************************** **
** Copyright notice **
** **
** (c) 2005-2006 RSSOwl Development Team **
** http://www.rssowl.org/ **
** **
** All rights reserved **
** **
** This program and the accompanying materials are made available under **
** the terms of the Eclipse Public License v1.0 which accompanies this **
** distribution, and is available at: **
** http://www.rssowl.org/legal/epl-v10.html **
** **
** A copy is found in the file epl-v10.html and important notices to the **
** license from the team is found in the textfile LICENSE.txt distributed **
** in this package. **
** **
** This copyright notice MUST APPEAR in all copies of the file! **
** **
** Contributors: **
** Christophe Bouhier - podcast plugin **
** **
** ********************************************************************** */
package org.rssowl.contrib.podcast.core.download;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.swt.widgets.Shell;
import org.rssowl.contrib.podcast.Activator;
import org.rssowl.contrib.podcast.core.net.INetTaskListener;
import org.rssowl.contrib.podcast.core.net.NetTask;
import org.rssowl.contrib.podcast.core.net.NetTaskEvent;
import org.rssowl.contrib.podcast.model.IPersonalAttachment;
import org.rssowl.contrib.podcast.model.IPersonalBookMark;
import org.rssowl.contrib.podcast.model.Model;
import org.rssowl.contrib.podcast.util.Logger;
import org.rssowl.contrib.podcast.util.PulseService;
import org.rssowl.core.persist.IAttachment;
import org.rssowl.core.persist.IBookMark;
import org.rssowl.core.persist.IFeed;
import org.rssowl.core.util.ITask;
import org.rssowl.core.util.JobQueue;
/**
* A download service. It manages a queue of downloads.
*
* This service registers itself as pulse listener. TODO, should de-register
* from the pulse. TODO, Notification of changes in the model should be closer
* to the collection.
*
* @author <a href="mailto:christophe@kualasoft.com">Christophe Bouhier </a>
* @author <a href="mailto:andreas.schaefer@madplanet.com">Andreas Schaefer </a>
* @version 1.1
*/
public class DownloadService implements INetTaskListener, ActionListener {
public static final int CONNECTING = 0; // Active state
public static final int DOWNLOADING = 1; // Active state
public static final int RETRYING = 2; // Active state
public static final int ERROR = 3; // Passive state
public static final int CANCELLED = 4; // Passive state
public static final int COMPLETED = 5; // Passive state
public static final int QUEUED = 6; // Active state
public static final int IDLE = 7; // Passive state
public static final int PAUZED = 8; // Passive state
public static final int RELEASING = 9; // Activate state
public static final String[] STATE_DESCRIPTION = { "CONNECTING",
"DOWNLOADING", "RETRYING", "ERROR", "CANCELLED", "COMPLETED",
"QUEUED", "IDLE", "PAUZED", "RELEASING" };
/* Max. number of concurrent running reload Jobs */
private static final int MAX_CONCURRENT_DOWNLOAD_JOBS = 5;
/* Queue for downloading from Feeds */
private final JobQueue fDownloadQueue;
/* list of downloads */
private List<IDownload> mDownloadList = new CopyOnWriteArrayList<IDownload>();
private List<IDownloadListener> mDownloadListeners = new CopyOnWriteArrayList<IDownloadListener>();
private static DownloadService sSelf;
private boolean mPauzed = false;
private Logger mLog = new Logger(DownloadService.class.getName());
public static DownloadService getInstance() {
if (sSelf == null) {
sSelf = new DownloadService();
}
return sSelf;
}
// Signature changed, added maxqueue size.
public DownloadService() {
fDownloadQueue = new JobQueue("Downloading podcasts",
MAX_CONCURRENT_DOWNLOAD_JOBS, 5, true, 0);
PulseService.getInstance().addEverySecondListener(this);
}
public void stopService() {
PulseService.getInstance().removeEverySecondListener(this);
}
protected void notifyDownloadEvent(DownloadEvent pEvent) {
Iterator<IDownloadListener> lIt = mDownloadListeners.iterator();
for (; lIt.hasNext();) {
IDownloadListener lListener = lIt.next();
if (mDownloadListeners.contains(lListener)) {
lListener.modelChanged(pEvent);
}
}
}
public void queueJob(ITask pTask) {
/* Check if Task is not yet Queued already */
if (!fDownloadQueue.isQueued(pTask))
fDownloadQueue.schedule(pTask);
}
/**
* The attachment should be of type IPersonalAttachment. We create one if
* needed.
*
* @param pAttachment
* @param shell
*/
public void queueJob(IPersonalAttachment pAttachment, final Shell shell) {
Download lTask = null;
IPersonalAttachment lPAttachment;
if (!(pAttachment instanceof IPersonalAttachment)) {
lPAttachment = (IPersonalAttachment) pAttachment;
}else{
lPAttachment = pAttachment;
}
lTask = new Download(lPAttachment, shell, ITask.Priority.DEFAULT);
/* Check if Task is not yet Queued already */
if (!fDownloadQueue.isQueued(lTask))
fDownloadQueue.schedule(lTask);
}
/**
* @param pListener
*/
public void addListener(IDownloadListener pListener) {
if (!mDownloadListeners.contains(pListener)) {
mDownloadListeners.add(pListener);
}
}
/**
* @param pListener
*/
public void removeListener(IDownloadListener pListener) {
if (mDownloadListeners.contains(pListener)) {
mDownloadListeners.remove(pListener);
}
}
/**
* Add an attachment to be downloaded.
*
* @param pPAttachment
* @return
*/
/*
* public IDownload addDownload(IPersonalAttachment pPAttachment) { Download
* lDownload = new Download(pPAttachment);
*
* download(lDownload); return lDownload; }
*/
public void abort(IPersonalBookMark pPBookMark) {
for (IDownload lDownload : mDownloadList) {
IFeed lFeed = lDownload.getAttachment().getNews()
.getFeedReference().resolve();
IBookMark lPBookMark = Model.getInstance().getBookMark(lFeed);
if (lPBookMark.equals(pPBookMark)) { // CB THIS equatation will
// not work.
abort(lDownload);
}
}
}
public void abort(IDownload pDownload) {
if (pDownload != null) {
int lState = pDownload.getState();
if (lState != COMPLETED || lState != ERROR) {
pDownload.setState(CANCELLED);
NetTask.getInstance().notifyNetworkEvent(
new NetTaskEvent(pDownload,
NetTaskEvent.DOWNLOAD_STATUS_CHANGED));
}
}
}
/**
* This method will create the folder and file to store the download. it
* will also handle possible errors in preparing the download file.
* Subsequently the download object is handed over to the network function.
*
* @param pDownload
* Downloads.Download
*/
public IStatus download(IDownload pDownload) {
if (mPauzed) {
return Status.CANCEL_STATUS; // Downloads are not added in status
// pauzed.
}
if(!mDownloadList.contains(pDownload)){
mDownloadList.add(mDownloadList.size(), pDownload);
notifyDownloadEvent(new DownloadEvent(this));
IPersonalAttachment lAttach = pDownload.getAttachment();
}
try {
int lState = pDownload.getState();
if (lState == CANCELLED) {
return Status.CANCEL_STATUS;
} else {
pDownload.setState(QUEUED);
NetTask.getInstance().downLoad(pDownload);
}
} catch (Exception ie) {
mLog.warn(ie.getMessage());
}
return Status.OK_STATUS;
}
/**
* Change the status of all downloads to pauze.
*/
public void pauzeAll() {
mPauzed = true;
for (IDownload lDownload : mDownloadList) {
int lState = lDownload.getState();
if (lState == DOWNLOADING || lState == QUEUED
|| lState == CONNECTING || lState == RETRYING) {
lDownload.setState(PAUZED);
NetTask.getInstance().notifyNetworkEvent(
new NetTaskEvent(lDownload,
NetTaskEvent.DOWNLOAD_STATUS_CHANGED));
}
}
}
/**
* Change the status of all downloads to pauze.
*/
public void resumeAll() {
mPauzed = false;
for (IDownload lDownload : mDownloadList) {
if (lDownload.getState() == PAUZED) {
lDownload.resetSpeed();
Download lTask = (Download)lDownload;
queueJob(lTask);
}
}
}
/**
* Clean all completed, errorneous and cancelled downloads.
*/
public void cleanAllCompleted() {
int lState;
for (IDownload download : mDownloadList) {
lState = download.getState();
if (lState == COMPLETED || lState == ERROR || lState == CANCELLED) {
mDownloadList.remove(download);
}
}
notifyDownloadEvent(new DownloadEvent(this));
}
/**
* Get a download class, based on the index.
*
* @param int
* pIndex
* @return Download
*/
public IDownload getDownload(int pIndex) {
return (IDownload) mDownloadList.get(pIndex);
}
public boolean isValidIndex(int pIndex) {
try {
mDownloadList.get(pIndex);
return true;
} catch (IndexOutOfBoundsException e) {
return false;
}
}
/**
* Get a download class, based on the enclosure.
*
* @param pAttachment
* Enclosure
* @return Download
*/
public IDownload getDownload(IAttachment pAttachment) {
for (IDownload lDownload : mDownloadList) {
if (lDownload.getAttachment() == pAttachment) {
return lDownload;
}
}
return null;
}
/**
* Get the download list.
*
* @return Object[]
*/
public Object[] getDownloadArray() {
return mDownloadList.toArray();
}
public int getDownloadIndexOf(IDownload pDownload) {
return mDownloadList.indexOf(pDownload);
}
/**
* Get the download list.
*
* @return LinkedList
*/
public Iterator<IDownload> getDownloadIterator() {
return mDownloadList.iterator();
}
public List<IDownload> getDownloadList() {
return mDownloadList;
}
/**
* The number of currently ongoing downloads;
*
* @return int
*/
public int getNumberOfPauzedDownloads() {
int count = 0;
for (IDownload lDownload : mDownloadList) {
count += lDownload.getState() == PAUZED ? 1 : 0;
}
return count;
}
/**
* The number of all entries in the download list.
*
* @return int
*/
public int getNumberOfDownloadItems() {
return mDownloadList.size();
}
/**
* The number of currently ongoing downloads; including status
* <code>DOWNLOADING</code>, <code>CONNECTING</code>,
* <code>QUEUED</code> and <code>RETRYING</code>
*
* @return int
*/
public int getNumberOfActiveDownloads() {
int count = 0;
for (IDownload lDownload : mDownloadList) {
int lState = lDownload.getState();
if (lState == DOWNLOADING || lState == RETRYING
|| lState == CONNECTING || lState == QUEUED) {
count++;
}
}
return count;
}
public boolean isDownloading() {
return getNumberOfActiveDownloads() == 0;
}
/**
* Get the index of the download from the service.
*
* @param pDownload
* @return
*/
public int indexOf(IDownload pDownload) {
return mDownloadList.indexOf(pDownload);
}
/**
* Query if a specific enclosure is in the download list and the status is
* downloading. This convenience class of of particular use before
* initiating a download to avoid a download conflict.
*
* @param pAttachment
* @return boolean If the enclosure is being downloaded.
*/
public boolean isDownloading(IPersonalAttachment pPAttachment) {
for (IDownload lDownload : mDownloadList) {
// CB TODO, check if the equal code complies to
// comparing the URL?
if (lDownload.getAttachment().equals(pPAttachment)) {
return true;
} else {
continue;
}
// CB FIXME remove later, should be embedded in the attachement
// model.
// try {
// file = dEncl.getURL().getFile();
// String cfile = encl.getURL().getFile();
// if (file.equals(cfile)) {
// return true;
// }
// } catch (XEnclosureException e) {
// }
}
return false;
}
/**
* Retry a download.(Not possible in status pauzed).
*/
public void retry(IDownload pDownload) {
if (!mPauzed) {
resetDownload(pDownload);
// CB TODO, We can't call download here. Use queue job instead.
// download(pDownload);
} else {
Activator.getDefault().getLog().log(
new Status(IStatus.INFO, getClass().getName(), IStatus.OK,
"Retry not allowed in status pauzed", null));
}
}
/**
* Check the status of this service.
*
* @return
*/
public boolean getPauzed() {
return mPauzed;
}
/**
* Reset the download object. (Basically same as a new download object).
*
* @param lDownload
* @return
*/
public IDownload resetDownload(IDownload pDownload) {
boolean match = false;
for (IDownload lDownload : mDownloadList) {
if (lDownload.equals(lDownload)) {
match = true;
break;
}
}
if (match) {
pDownload.setState(IDLE);
pDownload.resetTimeElapsed();
pDownload.resetSpeed();
pDownload.setCurrent(0);
}
return pDownload;
}
/**
* Listen for network events.
*
* @see com.jpodder.net.INetTaskListener#netActionPerformed(com.jpodder.net.NetTaskEvent)
*/
public void netActionPerformed(NetTaskEvent event) {
Download lDownload = (Download) event.getSource();
int lState = lDownload.getState();
IPersonalAttachment lAttachment = lDownload.getAttachment();
if (event.getNetEvent() == NetTaskEvent.DOWNLOAD_SUCCESS) {
// CB TODO, decide what to do with the cache.
// Cache.getInstance().addTrack(lEnclosure.getFile());
lAttachment.setCached(true);
lAttachment.setDownloadCompleted(true);
lAttachment.setLocal(true);
lAttachment.setMarked(false);
lAttachment.setCandidate(false);
// CB TODO, review if this is the option.
// lEnclosure.getFeed().setCandidatesCount(
// lEnclosure.getFeed().getCandidatesCount() - 1);
mLog.info("Download of: " + lAttachment.getFile() + " completed");
// CB TODO Migrate post-download handling.
// if (ContentLogic.isTorrent(lEnclosure.getFile().getName())) {
// mLog.info("Process torrent file.");
// lEnclosure.setTorrent();
// } else {
// mLog.info("Process file (non-torrent).");
//
// // CB TODO In Concurrency, we would like to exit this thread and
// // let another thread post-process the download. Using
// // semaphores? Also which tasks are executed should be
// // configurable.
//
// ID3Logic.getInstance().rewriteTags(lEnclosure);
// PlayerLogic.getInstance().storeInPlayer(lEnclosure);
// }
}
if (event.getNetEvent() == NetTaskEvent.DOWNLOAD_STATUS_CHANGED) {
mLog.info("Download of: " + lAttachment + " changed state to: "
+ STATE_DESCRIPTION[lState]);
if (event.getNetEvent() == NetTaskEvent.DOWNLOAD_FAILED) {
mLog.error("Error while downloading " + lAttachment);
// retry the download.
if (event.getException() != null) {
String message = event.getException().getMessage();
mLog.error(message);
if (message != null) {
if (message.startsWith("Read timed")
|| message.startsWith("Partial content")
|| message.startsWith("Connection reset")
|| message.startsWith("Connection timed out")) {
DownloadsRetry lRetry = new DownloadsRetry();
lRetry.retryLater(lDownload);
}
}
}
// Delete the file if the download failed totally (size =0).
File file = lAttachment.getFile();
if (file != null && file.exists() && file.length() == 0) {
file.delete();
}
}
// Do some state updating on the model.
switch (lState) {
case DOWNLOADING:
case QUEUED:
case CONNECTING:
case RELEASING:
break;
case RETRYING:
case PAUZED: {
lDownload.mSpeed = 0;
}
break;
case ERROR:
case CANCELLED: {
lDownload.mSpeed = 0;
}
break;
case COMPLETED: {
lDownload.mSpeed = 0;
// CB TODO, migrate
// if (Configuration.getInstance().getSound()) {
// Toolkit.getDefaultToolkit().beep();
// }
}
break;
}
}
}
/**
* Update elapsed time (+ 1 second) and bandwith for each download.
*
*/
public void heartBeat() {
synchronized (mDownloadList) {
for (IDownload lDownload : mDownloadList) {
switch (lDownload.getState()) {
case COMPLETED:
case QUEUED:
case CANCELLED:
case PAUZED:
case ERROR:
break;
case CONNECTING:
case RELEASING:
case RETRYING: {
lDownload.incrementSecond();
break;
}
case DOWNLOADING: {
lDownload.incrementSecond();
lDownload.calculateSpeed();
if (lDownload.getPrevious() != lDownload.getCurrent()) {
lDownload.setPrevious(lDownload.getCurrent());
}
}
break;
}
}
}
}
/**
* Hearbeat action.
*/
public void actionPerformed(ActionEvent e) {
heartBeat();
}
}