package de.danoeh.antennapod.core.storage;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.URLUtil;
import org.apache.commons.io.FilenameUtils;
import java.io.File;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedFile;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.util.FileNameGenerator;
import de.danoeh.antennapod.core.util.URLChecker;
/**
* Sends download requests to the DownloadService. This class should always be used for starting downloads,
* otherwise they won't work correctly.
*/
public class DownloadRequester {
private static final String TAG = "DownloadRequester";
public static final String IMAGE_DOWNLOADPATH = "images/";
public static final String FEED_DOWNLOADPATH = "cache/";
public static final String MEDIA_DOWNLOADPATH = "media/";
/**
* Denotes the page of the feed that is contained in the DownloadRequest sent by the DownloadRequester.
*/
public static final String REQUEST_ARG_PAGE_NR = "page";
/**
* True if all pages after the feed that is contained in this DownloadRequest should be downloaded.
*/
public static final String REQUEST_ARG_LOAD_ALL_PAGES = "loadAllPages";
private static DownloadRequester downloader;
private Map<String, DownloadRequest> downloads;
private DownloadRequester() {
downloads = new ConcurrentHashMap<>();
}
public static synchronized DownloadRequester getInstance() {
if (downloader == null) {
downloader = new DownloadRequester();
}
return downloader;
}
/**
* Starts a new download with the given DownloadRequest. This method should only
* be used from outside classes if the DownloadRequest was created by the DownloadService to
* ensure that the data is valid. Use downloadFeed(), downloadImage() or downloadMedia() instead.
*
* @param context Context object for starting the DownloadService
* @param request The DownloadRequest. If another DownloadRequest with the same source URL is already stored, this method
* call will return false.
* @return True if the download request was accepted, false otherwise.
*/
public synchronized boolean download(@NonNull Context context,
@NonNull DownloadRequest request) {
if (downloads.containsKey(request.getSource())) {
if (BuildConfig.DEBUG) Log.i(TAG, "DownloadRequest is already stored.");
return false;
}
downloads.put(request.getSource(), request);
Intent launchIntent = new Intent(context, DownloadService.class);
launchIntent.putExtra(DownloadService.EXTRA_REQUEST, request);
context.startService(launchIntent);
return true;
}
private void download(Context context, FeedFile item, FeedFile container, File dest,
boolean overwriteIfExists, String username, String password,
String lastModified, boolean deleteOnFailure, Bundle arguments) {
final boolean partiallyDownloadedFileExists = item.getFile_url() != null;
if (isDownloadingFile(item)) {
Log.e(TAG, "URL " + item.getDownload_url()
+ " is already being downloaded");
return;
}
if (!isFilenameAvailable(dest.toString()) || (!partiallyDownloadedFileExists && dest.exists())) {
Log.d(TAG, "Filename already used.");
if (isFilenameAvailable(dest.toString()) && overwriteIfExists) {
boolean result = dest.delete();
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.EXTENSION_SEPARATOR
+ FilenameUtils.getExtension(dest.getName());
Log.d(TAG, "Testing filename " + newName);
newDest = new File(dest.getParent(), newName);
if (!newDest.exists()
&& isFilenameAvailable(newDest.toString())) {
Log.d(TAG, "File doesn't exist yet. Using " + newName);
break;
}
}
if (newDest != null) {
dest = newDest;
}
}
}
Log.d(TAG, "Requesting download of url " + item.getDownload_url());
String baseUrl = (container != null) ? container.getDownload_url() : null;
item.setDownload_url(URLChecker.prepareURL(item.getDownload_url(), baseUrl));
DownloadRequest.Builder builder = new DownloadRequest.Builder(dest.toString(), item)
.withAuthentication(username, password)
.lastModified(lastModified)
.deleteOnFailure(deleteOnFailure)
.withArguments(arguments);
DownloadRequest request = builder.build();
download(context, request);
}
/**
* 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 (TextUtils.equals(r.getDestination(), path)) {
if (BuildConfig.DEBUG)
Log.d(TAG, path
+ " is already used by another requested download");
return false;
}
}
if (BuildConfig.DEBUG)
Log.d(TAG, path + " is available as a download destination");
return true;
}
/**
* Downloads a feed
*
* @param context The application's environment.
* @param feed Feed to download
* @param loadAllPages Set to true to download all pages
*/
public synchronized void downloadFeed(Context context, Feed feed, boolean loadAllPages,
boolean force)
throws DownloadRequestException {
if (feedFileValid(feed)) {
String username = (feed.getPreferences() != null) ? feed.getPreferences().getUsername() : null;
String password = (feed.getPreferences() != null) ? feed.getPreferences().getPassword() : null;
String lastModified = feed.isPaged() || force ? null : feed.getLastUpdate();
Bundle args = new Bundle();
args.putInt(REQUEST_ARG_PAGE_NR, feed.getPageNr());
args.putBoolean(REQUEST_ARG_LOAD_ALL_PAGES, loadAllPages);
download(context, feed, null, new File(getFeedfilePath(context),
getFeedfileName(feed)), true, username, password, lastModified, true, args);
}
}
public synchronized void downloadFeed(Context context, Feed feed) throws DownloadRequestException {
downloadFeed(context, feed, false, false);
}
public synchronized void downloadMedia(Context context, FeedMedia feedmedia)
throws DownloadRequestException {
if (feedFileValid(feedmedia)) {
Feed feed = feedmedia.getItem().getFeed();
String username;
String password;
if (feed != null && feed.getPreferences() != null) {
username = feed.getPreferences().getUsername();
password = feed.getPreferences().getPassword();
} else {
username = null;
password = null;
}
File dest;
if (feedmedia.getFile_url() != null) {
dest = new File(feedmedia.getFile_url());
} else {
dest = new File(getMediafilePath(context, feedmedia),
getMediafilename(feedmedia));
}
download(context, feedmedia, feed,
dest, false, username, password, null, false, null);
}
}
/**
* 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 synchronized void cancelDownload(final Context context, final FeedFile f) {
cancelDownload(context, f.getDownload_url());
}
/**
* Cancels a running download.
*/
public synchronized void cancelDownload(final Context context, final String downloadUrl) {
if (BuildConfig.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 synchronized void cancelAllDownloads(Context context) {
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 synchronized 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 synchronized boolean isDownloadingFile(FeedFile item) {
return item.getDownload_url() != null && downloads.containsKey(item.getDownload_url());
}
public synchronized DownloadRequest getDownload(String downloadUrl) {
return downloads.get(downloadUrl);
}
/**
* Checks if feedfile with the given download url is in the downloads list
*/
public synchronized boolean isDownloadingFile(String downloadUrl) {
return downloads.get(downloadUrl) != null;
}
public synchronized boolean hasNoDownloads() {
return downloads.isEmpty();
}
/**
* Remove an object from the downloads-list of the requester.
*/
public synchronized 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 synchronized int getNumberOfDownloads() {
return downloads.size();
}
public synchronized String getFeedfilePath(Context context)
throws DownloadRequestException {
return getExternalFilesDirOrThrowException(context, FEED_DOWNLOADPATH)
.toString() + "/";
}
public synchronized 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 synchronized 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(type);
if (result == null) {
throw new DownloadRequestException(
"Failed to access external storage");
}
return result;
}
private String getMediafilename(FeedMedia media) {
String filename;
String titleBaseFilename = "";
// Try to generate the filename by the item title
if (media.getItem() != null && media.getItem().getTitle() != null) {
String title = media.getItem().getTitle();
// Delete reserved characters
titleBaseFilename = title.replaceAll("[^a-zA-Z0-9 ._()-]", "");
titleBaseFilename = titleBaseFilename.trim();
}
String URLBaseFilename = URLUtil.guessFileName(media.getDownload_url(),
null, media.getMime_type());
if (!titleBaseFilename.equals("")) {
// Append extension
final int FILENAME_MAX_LENGTH = 220;
if (titleBaseFilename.length() > FILENAME_MAX_LENGTH) {
titleBaseFilename = titleBaseFilename.substring(0, FILENAME_MAX_LENGTH);
}
filename = titleBaseFilename + FilenameUtils.EXTENSION_SEPARATOR +
FilenameUtils.getExtension(URLBaseFilename);
} else {
// Fall back on URL file name
filename = URLBaseFilename;
}
return filename;
}
}