Mirror support for the index and app downloads

* App and index downloads fall back to a list of mirrors defined
  by the repository.
* The changes have been made trying to keep the original download
  code untouched, and only using the mirror logic when the download
  fails due to a connection error / timeout.
* The mirrors are tried in a randomized manner, and with proper
  timeouts. The download is aborted after the tries exceed the
  number of mirrors, times 3 for a total of 3 different timeout
  values (10s, 30s, and 1m)
* The mirror code isn't used for any images yet, most of which is
  handled by an external library.

Closes: #35
This commit is contained in:
Chirayu Desai 2017-07-07 19:01:52 +05:30
parent e310810f22
commit a160476a14
7 changed files with 236 additions and 26 deletions

View File

@ -55,6 +55,7 @@ import org.fdroid.fdroid.compat.PRNGFixes;
import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.InstalledAppProviderService; import org.fdroid.fdroid.data.InstalledAppProviderService;
import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.data.SanitizedFile; import org.fdroid.fdroid.data.SanitizedFile;
import org.fdroid.fdroid.installer.ApkFileProvider; import org.fdroid.fdroid.installer.ApkFileProvider;
import org.fdroid.fdroid.installer.InstallHistoryService; import org.fdroid.fdroid.installer.InstallHistoryService;
@ -93,6 +94,10 @@ public class FDroidApp extends Application {
public static volatile String bssid; public static volatile String bssid;
public static volatile Repo repo = new Repo(); public static volatile Repo repo = new Repo();
private static volatile String lastWorkingMirror = null;
private static volatile int numTries = Integer.MAX_VALUE;
private static volatile int timeout = 10000;
// Leaving the fully qualified class name here to help clarify the difference between spongy/bouncy castle. // Leaving the fully qualified class name here to help clarify the difference between spongy/bouncy castle.
private static final org.spongycastle.jce.provider.BouncyCastleProvider SPONGYCASTLE_PROVIDER; private static final org.spongycastle.jce.provider.BouncyCastleProvider SPONGYCASTLE_PROVIDER;
@ -200,6 +205,53 @@ public class FDroidApp extends Application {
repo = new Repo(); repo = new Repo();
} }
public static String getMirror(String urlString, long repoId) throws IOException {
return getMirror(urlString, RepoProvider.Helper.findById(getInstance(), repoId));
}
public static String getMirror(String urlString, Repo repo2) throws IOException {
if (repo2.hasMirrors()) {
if (lastWorkingMirror == null) {
lastWorkingMirror = repo2.address;
}
if (numTries <= 0) {
if (timeout == 10000) {
timeout = 30000;
numTries = Integer.MAX_VALUE;
} else if (timeout == 30000) {
timeout = 60000;
numTries = Integer.MAX_VALUE;
} else {
Utils.debugLog(TAG, "Mirrors: Giving up");
throw new IOException("Ran out of mirrors");
}
}
if (numTries == Integer.MAX_VALUE) {
numTries = repo2.getMirrorCount();
}
String mirror = repo2.getMirror(lastWorkingMirror);
String newUrl = urlString.replace(lastWorkingMirror, mirror);
Utils.debugLog(TAG, "Trying mirror " + mirror + " after " + lastWorkingMirror + " failed," +
" timeout=" + timeout / 1000 + "s");
lastWorkingMirror = mirror;
numTries--;
return newUrl;
} else {
throw new IOException("No mirrors available");
}
}
public static int getTimeout() {
return timeout;
}
public static void resetMirrorVars() {
// Reset last working mirror, numtries, and timeout
lastWorkingMirror = null;
numTries = Integer.MAX_VALUE;
timeout = 10000;
}
@Override @Override
public void onConfigurationChanged(Configuration newConfig) { public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig); super.onConfigurationChanged(newConfig);

View File

@ -6,6 +6,7 @@ import android.net.Uri;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonFactory;
@ -14,6 +15,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.InjectableValues; import com.fasterxml.jackson.databind.InjectableValues;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.App;
@ -24,8 +26,11 @@ import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.net.Downloader; import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.DownloaderFactory; import org.fdroid.fdroid.net.DownloaderFactory;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.URL; import java.net.URL;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.ArrayList; import java.util.ArrayList;
@ -78,7 +83,6 @@ public class IndexV1Updater extends RepoUpdater {
return false; return false;
} }
Downloader downloader = null; Downloader downloader = null;
InputStream indexInputStream = null;
try { try {
// read file name from file // read file name from file
final Uri dataUri = Uri.parse(indexUrl); final Uri dataUri = Uri.parse(indexUrl);
@ -95,12 +99,47 @@ public class IndexV1Updater extends RepoUpdater {
return true; return true;
} }
JarFile jarFile = new JarFile(downloader.outputFile, true); processDownloadedIndex(downloader.outputFile, downloader.getCacheTag());
JarEntry indexEntry = (JarEntry) jarFile.getEntry(DATA_FILE_NAME); } catch (ConnectException | SocketTimeoutException e) {
indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry), Utils.debugLog(TAG, "Trying to download the index from a mirror");
processIndexListener, new URL(repo.address), (int) indexEntry.getSize()); // Mirror logic here, so that the default download code is untouched.
processIndexV1(indexInputStream, indexEntry, downloader.getCacheTag()); String mirrorUrl;
String prevMirrorUrl = indexUrl;
FDroidApp.resetMirrorVars();
int n = repo.getMirrorCount() * 3; // 3 is the number of timeouts we have. 10s, 30s & 60s
for (int i = 0; i <= n; i++) {
try {
mirrorUrl = FDroidApp.getMirror(prevMirrorUrl, repo);
prevMirrorUrl = mirrorUrl;
Uri dataUri2 = Uri.parse(mirrorUrl);
downloader = DownloaderFactory.create(context, dataUri2.toString());
downloader.setCacheTag(repo.lastetag);
downloader.setListener(downloadListener);
downloader.setTimeout(FDroidApp.getTimeout());
downloader.download();
if (downloader.isNotFound()) {
return false;
}
hasChanged = downloader.hasChanged();
if (!hasChanged) {
return true;
}
processDownloadedIndex(downloader.outputFile, downloader.getCacheTag());
break;
} catch (ConnectException | SocketTimeoutException e2) {
// We'll just let this try the next mirror
Utils.debugLog(TAG, "Trying next mirror");
} catch (IOException e2) {
if (downloader != null) {
FileUtils.deleteQuietly(downloader.outputFile);
}
throw new RepoUpdater.UpdateException(repo, "Error getting index file", e2);
} catch (InterruptedException e2) {
// ignored if canceled, the local database just won't be updated
}
}
} catch (IOException e) { } catch (IOException e) {
if (downloader != null) { if (downloader != null) {
FileUtils.deleteQuietly(downloader.outputFile); FileUtils.deleteQuietly(downloader.outputFile);
@ -113,6 +152,15 @@ public class IndexV1Updater extends RepoUpdater {
return true; return true;
} }
private void processDownloadedIndex(File outputFile, String cacheTag)
throws IOException, RepoUpdater.UpdateException {
JarFile jarFile = new JarFile(outputFile, true);
JarEntry indexEntry = (JarEntry) jarFile.getEntry(DATA_FILE_NAME);
InputStream indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry),
processIndexListener, new URL(repo.address), (int) indexEntry.getSize());
processIndexV1(indexInputStream, indexEntry, cacheTag);
}
/** /**
* Get the standard {@link ObjectMapper} instance used for parsing {@code index-v1.json}. * Get the standard {@link ObjectMapper} instance used for parsing {@code index-v1.json}.
* This ignores unknown properties so that old releases won't crash when new things are * This ignores unknown properties so that old releases won't crash when new things are

View File

@ -27,12 +27,16 @@ import android.content.ContentValues;
import android.database.Cursor; import android.database.Cursor;
import android.text.TextUtils; import android.text.TextUtils;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Schema.RepoTable.Cols; import org.fdroid.fdroid.data.Schema.RepoTable.Cols;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List;
/** /**
@ -297,4 +301,50 @@ public class Repo extends ValueObject {
pushRequests = toInt(values.getAsInteger(Cols.PUSH_REQUESTS)); pushRequests = toInt(values.getAsInteger(Cols.PUSH_REQUESTS));
} }
} }
public boolean hasMirrors() {
return mirrors != null && mirrors.length > 1;
}
public int getMirrorCount() {
int count = 0;
if (mirrors != null && mirrors.length > 1) {
for (String m: mirrors) {
if (!m.equals(address)) {
if (FDroidApp.isUsingTor()) {
count++;
} else {
if (!m.contains(".onion")) {
count++;
}
}
}
}
}
return count;
}
public String getMirror(String lastWorkingMirror) {
if (TextUtils.isEmpty(lastWorkingMirror)) {
lastWorkingMirror = address;
}
List<String> shuffledMirrors = Arrays.asList(mirrors);
Collections.shuffle(shuffledMirrors);
if (shuffledMirrors.size() > 1) {
for (String m : shuffledMirrors) {
// Return a non default, and not last used mirror
if (!m.equals(address) && !m.equals(lastWorkingMirror)) {
if (FDroidApp.isUsingTor()) {
return m;
} else {
// Filter-out onion mirrors for non-tor connections
if (!m.contains(".onion")) {
return m;
}
}
}
}
}
return null; // In case we are out of mirrors.
}
} }

View File

@ -15,6 +15,7 @@ import android.text.TextUtils;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.WildcardFileFilter; import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.fdroid.fdroid.AppUpdateStatusManager; import org.fdroid.fdroid.AppUpdateStatusManager;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Hasher; import org.fdroid.fdroid.Hasher;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.compat.PackageManagerCompat; import org.fdroid.fdroid.compat.PackageManagerCompat;
@ -167,6 +168,9 @@ public class InstallManagerService extends Service {
return START_NOT_STICKY; return START_NOT_STICKY;
} }
FDroidApp.resetMirrorVars();
DownloaderService.setTimeout(FDroidApp.getTimeout());
appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.Downloading, null); appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.Downloading, null);
appUpdateStatusManager.markAsPendingInstall(urlString); appUpdateStatusManager.markAsPendingInstall(urlString);
@ -178,7 +182,7 @@ public class InstallManagerService extends Service {
long apkFileSize = apkFilePath.length(); long apkFileSize = apkFilePath.length();
if (!apkFilePath.exists() || apkFileSize < apk.size) { if (!apkFilePath.exists() || apkFileSize < apk.size) {
Utils.debugLog(TAG, "download " + urlString + " " + apkFilePath); Utils.debugLog(TAG, "download " + urlString + " " + apkFilePath);
DownloaderService.queue(this, urlString); DownloaderService.queue(this, urlString, apk.repoId, urlString);
} else if (ApkCache.apkIsCached(apkFilePath, apk)) { } else if (ApkCache.apkIsCached(apkFilePath, apk)) {
Utils.debugLog(TAG, "skip download, we have it, straight to install " + urlString + " " + apkFilePath); Utils.debugLog(TAG, "skip download, we have it, straight to install " + urlString + " " + apkFilePath);
sendBroadcast(intent.getData(), Downloader.ACTION_STARTED, apkFilePath); sendBroadcast(intent.getData(), Downloader.ACTION_STARTED, apkFilePath);
@ -186,8 +190,9 @@ public class InstallManagerService extends Service {
} else { } else {
Utils.debugLog(TAG, "delete and download again " + urlString + " " + apkFilePath); Utils.debugLog(TAG, "delete and download again " + urlString + " " + apkFilePath);
apkFilePath.delete(); apkFilePath.delete();
DownloaderService.queue(this, urlString); DownloaderService.queue(this, urlString, apk.repoId, urlString);
} }
return START_REDELIVER_INTENT; // if killed before completion, retry Intent return START_REDELIVER_INTENT; // if killed before completion, retry Intent
} }
@ -251,12 +256,14 @@ public class InstallManagerService extends Service {
} }
} else if (Downloader.ACTION_INTERRUPTED.equals(action)) { } else if (Downloader.ACTION_INTERRUPTED.equals(action)) {
localBroadcastManager.unregisterReceiver(this); localBroadcastManager.unregisterReceiver(this);
} else if (Downloader.ACTION_CONNECTION_FAILED.equals(action)) {
DownloaderService.queue(context, urlString, 0, urlString);
} else { } else {
throw new RuntimeException("intent action not handled!"); throw new RuntimeException("intent action not handled!");
} }
} }
}; };
DownloaderService.queue(this, obbUrlString); DownloaderService.queue(this, obbUrlString, 0, obbUrlString);
localBroadcastManager.registerReceiver(downloadReceiver, localBroadcastManager.registerReceiver(downloadReceiver,
DownloaderService.getIntentFilter(obbUrlString)); DownloaderService.getIntentFilter(obbUrlString));
} }
@ -268,6 +275,8 @@ public class InstallManagerService extends Service {
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
Uri downloadUri = intent.getData(); Uri downloadUri = intent.getData();
String urlString = downloadUri.toString(); String urlString = downloadUri.toString();
long repoId = intent.getLongExtra(Downloader.EXTRA_REPO_ID, 0);
String mirrorUrlString = intent.getStringExtra(Downloader.EXTRA_MIRROR_URL);
switch (intent.getAction()) { switch (intent.getAction()) {
case Downloader.ACTION_STARTED: case Downloader.ACTION_STARTED:
@ -287,7 +296,7 @@ public class InstallManagerService extends Service {
File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH)); File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
Uri localApkUri = Uri.fromFile(localFile); Uri localApkUri = Uri.fromFile(localFile);
Utils.debugLog(TAG, "download completed of " + urlString + " to " + localApkUri); Utils.debugLog(TAG, "download completed of " + mirrorUrlString + " to " + localApkUri);
appUpdateStatusManager.updateApk(urlString, AppUpdateStatusManager.Status.ReadyToInstall, null); appUpdateStatusManager.updateApk(urlString, AppUpdateStatusManager.Status.ReadyToInstall, null);
localBroadcastManager.unregisterReceiver(this); localBroadcastManager.unregisterReceiver(this);
@ -303,6 +312,16 @@ public class InstallManagerService extends Service {
appUpdateStatusManager.setDownloadError(urlString, intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE)); appUpdateStatusManager.setDownloadError(urlString, intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE));
localBroadcastManager.unregisterReceiver(this); localBroadcastManager.unregisterReceiver(this);
break; break;
case Downloader.ACTION_CONNECTION_FAILED:
try {
DownloaderService.queue(context, FDroidApp.getMirror(mirrorUrlString, repoId), repoId, urlString);
DownloaderService.setTimeout(FDroidApp.getTimeout());
} catch (IOException e) {
appUpdateStatusManager.markAsNoLongerPendingInstall(urlString);
appUpdateStatusManager.setDownloadError(urlString, intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE));
localBroadcastManager.unregisterReceiver(this);
}
break;
default: default:
throw new RuntimeException("intent action not handled!"); throw new RuntimeException("intent action not handled!");
} }

View File

@ -8,6 +8,7 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.ConnectException;
import java.net.URL; import java.net.URL;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
@ -19,12 +20,16 @@ public abstract class Downloader {
public static final String ACTION_STARTED = "org.fdroid.fdroid.net.Downloader.action.STARTED"; public static final String ACTION_STARTED = "org.fdroid.fdroid.net.Downloader.action.STARTED";
public static final String ACTION_PROGRESS = "org.fdroid.fdroid.net.Downloader.action.PROGRESS"; public static final String ACTION_PROGRESS = "org.fdroid.fdroid.net.Downloader.action.PROGRESS";
public static final String ACTION_INTERRUPTED = "org.fdroid.fdroid.net.Downloader.action.INTERRUPTED"; public static final String ACTION_INTERRUPTED = "org.fdroid.fdroid.net.Downloader.action.INTERRUPTED";
public static final String ACTION_CONNECTION_FAILED = "org.fdroid.fdroid.net.Downloader.action.CONNECTION_FAILED";
public static final String ACTION_COMPLETE = "org.fdroid.fdroid.net.Downloader.action.COMPLETE"; public static final String ACTION_COMPLETE = "org.fdroid.fdroid.net.Downloader.action.COMPLETE";
public static final String EXTRA_DOWNLOAD_PATH = "org.fdroid.fdroid.net.Downloader.extra.DOWNLOAD_PATH"; public static final String EXTRA_DOWNLOAD_PATH = "org.fdroid.fdroid.net.Downloader.extra.DOWNLOAD_PATH";
public static final String EXTRA_BYTES_READ = "org.fdroid.fdroid.net.Downloader.extra.BYTES_READ"; public static final String EXTRA_BYTES_READ = "org.fdroid.fdroid.net.Downloader.extra.BYTES_READ";
public static final String EXTRA_TOTAL_BYTES = "org.fdroid.fdroid.net.Downloader.extra.TOTAL_BYTES"; public static final String EXTRA_TOTAL_BYTES = "org.fdroid.fdroid.net.Downloader.extra.TOTAL_BYTES";
public static final String EXTRA_ERROR_MESSAGE = "org.fdroid.fdroid.net.Downloader.extra.ERROR_MESSAGE"; public static final String EXTRA_ERROR_MESSAGE = "org.fdroid.fdroid.net.Downloader.extra.ERROR_MESSAGE";
public static final String EXTRA_REPO_ID = "org.fdroid.fdroid.net.Downloader.extra.ERROR_REPO_ID";
public static final String EXTRA_CANONICAL_URL = "org.fdroid.fdroid.net.Downloader.extra.ERROR_CANONICAL_URL";
public static final String EXTRA_MIRROR_URL = "org.fdroid.fdroid.net.Downloader.extra.ERROR_MIRROR_URL";
private volatile boolean cancelled = false; private volatile boolean cancelled = false;
private volatile int bytesRead; private volatile int bytesRead;
@ -36,6 +41,8 @@ public abstract class Downloader {
String cacheTag; String cacheTag;
boolean notFound; boolean notFound;
private volatile int timeout = 10000;
/** /**
* For sending download progress, should only be called in {@link #progressTask} * For sending download progress, should only be called in {@link #progressTask}
*/ */
@ -58,6 +65,14 @@ public abstract class Downloader {
this.downloaderProgressListener = listener; this.downloaderProgressListener = listener;
} }
public void setTimeout(int ms) {
timeout = ms;
}
public int getTimeout() {
return timeout;
}
/** /**
* If you ask for the cacheTag before calling download(), you will get the * If you ask for the cacheTag before calling download(), you will get the
* same one you passed in (if any). If you call it after download(), you * same one you passed in (if any). If you call it after download(), you
@ -79,7 +94,7 @@ public abstract class Downloader {
protected abstract int totalDownloadSize(); protected abstract int totalDownloadSize();
public abstract void download() throws IOException, InterruptedException; public abstract void download() throws ConnectException, IOException, InterruptedException;
/** /**
* @return whether the requested file was not found in the repo (e.g. HTTP 404 Not Found) * @return whether the requested file was not found in the repo (e.g. HTTP 404 Not Found)

View File

@ -40,17 +40,19 @@ import org.fdroid.fdroid.installer.ApkCache;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.URL; import java.net.URL;
/** /**
* DownloaderService is a service that handles asynchronous download requests * DownloaderService is a service that handles asynchronous download requests
* (expressed as {@link Intent}s) on demand. Clients send download requests * (expressed as {@link Intent}s) on demand. Clients send download requests
* through {@link #queue(Context, String)} calls. The * through {@link #queue(Context, String, long, String)} calls. The
* service is started as needed, it handles each {@code Intent} using a worker * service is started as needed, it handles each {@code Intent} using a worker
* thread, and stops itself when it runs out of work. Requests can be canceled * thread, and stops itself when it runs out of work. Requests can be canceled
* using {@link #cancel(Context, String)}. If this service is killed during * using {@link #cancel(Context, String)}. If this service is killed during
* operation, it will receive the queued {@link #queue(Context, String)} and * operation, it will receive the queued {@link #queue(Context, String, long, String)}
* {@link #cancel(Context, String)} requests again due to * and {@link #cancel(Context, String)} requests again due to
* {@link Service#START_REDELIVER_INTENT}. Bad requests will be ignored, * {@link Service#START_REDELIVER_INTENT}. Bad requests will be ignored,
* including on restart after killing via {@link Service#START_NOT_STICKY}. * including on restart after killing via {@link Service#START_NOT_STICKY}.
* <p> * <p>
@ -86,6 +88,7 @@ public class DownloaderService extends Service {
private static volatile ServiceHandler serviceHandler; private static volatile ServiceHandler serviceHandler;
private static volatile Downloader downloader; private static volatile Downloader downloader;
private LocalBroadcastManager localBroadcastManager; private LocalBroadcastManager localBroadcastManager;
private static volatile int timeout;
private final class ServiceHandler extends Handler { private final class ServiceHandler extends Handler {
ServiceHandler(Looper looper) { ServiceHandler(Looper looper) {
@ -188,7 +191,9 @@ public class DownloaderService extends Service {
private void handleIntent(Intent intent) { private void handleIntent(Intent intent) {
final Uri uri = intent.getData(); final Uri uri = intent.getData();
final SanitizedFile localFile = ApkCache.getApkDownloadPath(this, uri); final SanitizedFile localFile = ApkCache.getApkDownloadPath(this, uri);
sendBroadcast(uri, Downloader.ACTION_STARTED, localFile); long repoId = intent.getLongExtra(Downloader.EXTRA_REPO_ID, 0);
String originalUrlString = intent.getStringExtra(Downloader.EXTRA_CANONICAL_URL);
sendBroadcast(uri, Downloader.ACTION_STARTED, localFile, repoId, originalUrlString);
try { try {
downloader = DownloaderFactory.create(this, uri, localFile); downloader = DownloaderFactory.create(this, uri, localFile);
@ -202,18 +207,22 @@ public class DownloaderService extends Service {
localBroadcastManager.sendBroadcast(intent); localBroadcastManager.sendBroadcast(intent);
} }
}); });
downloader.setTimeout(timeout);
downloader.download(); downloader.download();
if (downloader.isNotFound()) { if (downloader.isNotFound()) {
sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile, getString(R.string.download_404)); sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile, getString(R.string.download_404),
repoId, originalUrlString);
} else { } else {
sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile); sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile, repoId, originalUrlString);
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile); sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile, repoId, originalUrlString);
} catch (ConnectException | SocketTimeoutException e) {
sendBroadcast(uri, Downloader.ACTION_CONNECTION_FAILED, localFile, repoId, originalUrlString);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile, sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile,
e.getLocalizedMessage()); e.getLocalizedMessage(), repoId, originalUrlString);
} finally { } finally {
if (downloader != null) { if (downloader != null) {
downloader.close(); downloader.close();
@ -226,19 +235,26 @@ public class DownloaderService extends Service {
sendBroadcast(uri, action, null, null); sendBroadcast(uri, action, null, null);
} }
private void sendBroadcast(Uri uri, String action, File file) { private void sendBroadcast(Uri uri, String action, File file, long repoId, String originalUrlString) {
sendBroadcast(uri, action, file, null); sendBroadcast(uri, action, file, null, repoId, originalUrlString);
} }
private void sendBroadcast(Uri uri, String action, File file, String errorMessage) { private void sendBroadcast(Uri uri, String action, File file, String errorMessage) {
sendBroadcast(uri, action, file, errorMessage, 0, null);
}
private void sendBroadcast(Uri uri, String action, File file, String errorMessage, long repoId,
String originalUrlString) {
Intent intent = new Intent(action); Intent intent = new Intent(action);
intent.setData(uri); intent.setData(Uri.parse(originalUrlString));
if (file != null) { if (file != null) {
intent.putExtra(Downloader.EXTRA_DOWNLOAD_PATH, file.getAbsolutePath()); intent.putExtra(Downloader.EXTRA_DOWNLOAD_PATH, file.getAbsolutePath());
} }
if (!TextUtils.isEmpty(errorMessage)) { if (!TextUtils.isEmpty(errorMessage)) {
intent.putExtra(Downloader.EXTRA_ERROR_MESSAGE, errorMessage); intent.putExtra(Downloader.EXTRA_ERROR_MESSAGE, errorMessage);
} }
intent.putExtra(Downloader.EXTRA_REPO_ID, repoId);
intent.putExtra(Downloader.EXTRA_MIRROR_URL, uri.toString());
localBroadcastManager.sendBroadcast(intent); localBroadcastManager.sendBroadcast(intent);
} }
@ -251,7 +267,7 @@ public class DownloaderService extends Service {
* @param urlString The URL to add to the download queue * @param urlString The URL to add to the download queue
* @see #cancel(Context, String) * @see #cancel(Context, String)
*/ */
public static void queue(Context context, String urlString) { public static void queue(Context context, String urlString, long repoId, String originalUrlString) {
if (TextUtils.isEmpty(urlString)) { if (TextUtils.isEmpty(urlString)) {
return; return;
} }
@ -259,6 +275,8 @@ public class DownloaderService extends Service {
Intent intent = new Intent(context, DownloaderService.class); Intent intent = new Intent(context, DownloaderService.class);
intent.setAction(ACTION_QUEUE); intent.setAction(ACTION_QUEUE);
intent.setData(Uri.parse(urlString)); intent.setData(Uri.parse(urlString));
intent.putExtra(Downloader.EXTRA_REPO_ID, repoId);
intent.putExtra(Downloader.EXTRA_CANONICAL_URL, originalUrlString);
context.startService(intent); context.startService(intent);
} }
@ -269,7 +287,7 @@ public class DownloaderService extends Service {
* *
* @param context this app's {@link Context} * @param context this app's {@link Context}
* @param urlString The URL to remove from the download queue * @param urlString The URL to remove from the download queue
* @see #queue(Context, String) * @see #queue(Context, String, long, String)
*/ */
public static void cancel(Context context, String urlString) { public static void cancel(Context context, String urlString) {
if (TextUtils.isEmpty(urlString)) { if (TextUtils.isEmpty(urlString)) {
@ -304,6 +322,10 @@ public class DownloaderService extends Service {
return downloader != null && TextUtils.equals(urlString, downloader.sourceUrl.toString()); return downloader != null && TextUtils.equals(urlString, downloader.sourceUrl.toString());
} }
public static void setTimeout(int ms) {
timeout = ms;
}
/** /**
* Get a prepared {@link IntentFilter} for use for matching this service's action events. * Get a prepared {@link IntentFilter} for use for matching this service's action events.
* *
@ -316,6 +338,7 @@ public class DownloaderService extends Service {
intentFilter.addAction(Downloader.ACTION_PROGRESS); intentFilter.addAction(Downloader.ACTION_PROGRESS);
intentFilter.addAction(Downloader.ACTION_COMPLETE); intentFilter.addAction(Downloader.ACTION_COMPLETE);
intentFilter.addAction(Downloader.ACTION_INTERRUPTED); intentFilter.addAction(Downloader.ACTION_INTERRUPTED);
intentFilter.addAction(Downloader.ACTION_CONNECTION_FAILED);
intentFilter.addDataScheme(uri.getScheme()); intentFilter.addDataScheme(uri.getScheme());
intentFilter.addDataAuthority(uri.getHost(), String.valueOf(uri.getPort())); intentFilter.addDataAuthority(uri.getHost(), String.valueOf(uri.getPort()));
intentFilter.addDataPath(uri.getPath(), PatternMatcher.PATTERN_LITERAL); intentFilter.addDataPath(uri.getPath(), PatternMatcher.PATTERN_LITERAL);

View File

@ -14,8 +14,10 @@ import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.ConnectException;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL; import java.net.URL;
public class HttpDownloader extends Downloader { public class HttpDownloader extends Downloader {
@ -77,7 +79,7 @@ public class HttpDownloader extends Downloader {
* @see <a href="http://lucb1e.com/rp/cookielesscookies">Cookieless cookies</a> * @see <a href="http://lucb1e.com/rp/cookielesscookies">Cookieless cookies</a>
*/ */
@Override @Override
public void download() throws IOException, InterruptedException { public void download() throws ConnectException, IOException, InterruptedException {
// get the file size from the server // get the file size from the server
HttpURLConnection tmpConn = getConnection(); HttpURLConnection tmpConn = getConnection();
tmpConn.setRequestMethod("HEAD"); tmpConn.setRequestMethod("HEAD");
@ -126,7 +128,7 @@ public class HttpDownloader extends Downloader {
&& FDroidApp.subnetInfo.isInRange(host); // on the same subnet as we are && FDroidApp.subnetInfo.isInRange(host); // on the same subnet as we are
} }
private HttpURLConnection getConnection() throws IOException { private HttpURLConnection getConnection() throws SocketTimeoutException, IOException {
HttpURLConnection connection; HttpURLConnection connection;
if (isSwapUrl()) { if (isSwapUrl()) {
// swap never works with a proxy, its unrouted IP on the same subnet // swap never works with a proxy, its unrouted IP on the same subnet
@ -136,6 +138,7 @@ public class HttpDownloader extends Downloader {
} }
connection.setRequestProperty("User-Agent", "F-Droid " + BuildConfig.VERSION_NAME); connection.setRequestProperty("User-Agent", "F-Droid " + BuildConfig.VERSION_NAME);
connection.setConnectTimeout(getTimeout());
if (username != null && password != null) { if (username != null && password != null) {
// add authorization header from username / password if set // add authorization header from username / password if set