diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index 8047e0d0a..031f6d588 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -55,6 +55,7 @@ import org.fdroid.fdroid.compat.PRNGFixes; import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.InstalledAppProviderService; import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.data.SanitizedFile; import org.fdroid.fdroid.installer.ApkFileProvider; import org.fdroid.fdroid.installer.InstallHistoryService; @@ -93,6 +94,10 @@ public class FDroidApp extends Application { public static volatile String bssid; 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. private static final org.spongycastle.jce.provider.BouncyCastleProvider SPONGYCASTLE_PROVIDER; @@ -200,6 +205,53 @@ public class FDroidApp extends Application { 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 public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); diff --git a/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java b/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java index 046c7f38c..fd0f1563b 100644 --- a/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java +++ b/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java @@ -6,6 +6,7 @@ import android.net.Uri; import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; + import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; 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.InjectableValues; import com.fasterxml.jackson.databind.ObjectMapper; + import org.apache.commons.io.FileUtils; import org.fdroid.fdroid.data.Apk; 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.DownloaderFactory; +import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.net.ConnectException; +import java.net.SocketTimeoutException; import java.net.URL; import java.security.cert.X509Certificate; import java.util.ArrayList; @@ -78,7 +83,6 @@ public class IndexV1Updater extends RepoUpdater { return false; } Downloader downloader = null; - InputStream indexInputStream = null; try { // read file name from file final Uri dataUri = Uri.parse(indexUrl); @@ -95,12 +99,47 @@ public class IndexV1Updater extends RepoUpdater { return true; } - JarFile jarFile = new JarFile(downloader.outputFile, true); - JarEntry indexEntry = (JarEntry) jarFile.getEntry(DATA_FILE_NAME); - indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry), - processIndexListener, new URL(repo.address), (int) indexEntry.getSize()); - processIndexV1(indexInputStream, indexEntry, downloader.getCacheTag()); + processDownloadedIndex(downloader.outputFile, downloader.getCacheTag()); + } catch (ConnectException | SocketTimeoutException e) { + Utils.debugLog(TAG, "Trying to download the index from a mirror"); + // Mirror logic here, so that the default download code is untouched. + 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) { if (downloader != null) { FileUtils.deleteQuietly(downloader.outputFile); @@ -113,6 +152,15 @@ public class IndexV1Updater extends RepoUpdater { 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}. * This ignores unknown properties so that old releases won't crash when new things are diff --git a/app/src/main/java/org/fdroid/fdroid/data/Repo.java b/app/src/main/java/org/fdroid/fdroid/data/Repo.java index 1c408ce14..eafc06d78 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Repo.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Repo.java @@ -27,12 +27,16 @@ import android.content.ContentValues; import android.database.Cursor; import android.text.TextUtils; +import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Schema.RepoTable.Cols; import java.net.MalformedURLException; import java.net.URL; +import java.util.Arrays; +import java.util.Collections; import java.util.Date; +import java.util.List; /** @@ -297,4 +301,50 @@ public class Repo extends ValueObject { 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 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. + } } diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java index c8589d058..4acf5fcda 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -15,6 +15,7 @@ import android.text.TextUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.io.filefilter.WildcardFileFilter; import org.fdroid.fdroid.AppUpdateStatusManager; +import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Hasher; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.compat.PackageManagerCompat; @@ -167,6 +168,9 @@ public class InstallManagerService extends Service { return START_NOT_STICKY; } + FDroidApp.resetMirrorVars(); + DownloaderService.setTimeout(FDroidApp.getTimeout()); + appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.Downloading, null); appUpdateStatusManager.markAsPendingInstall(urlString); @@ -178,7 +182,7 @@ public class InstallManagerService extends Service { long apkFileSize = apkFilePath.length(); if (!apkFilePath.exists() || apkFileSize < apk.size) { Utils.debugLog(TAG, "download " + urlString + " " + apkFilePath); - DownloaderService.queue(this, urlString); + DownloaderService.queue(this, urlString, apk.repoId, urlString); } else if (ApkCache.apkIsCached(apkFilePath, apk)) { Utils.debugLog(TAG, "skip download, we have it, straight to install " + urlString + " " + apkFilePath); sendBroadcast(intent.getData(), Downloader.ACTION_STARTED, apkFilePath); @@ -186,8 +190,9 @@ public class InstallManagerService extends Service { } else { Utils.debugLog(TAG, "delete and download again " + urlString + " " + apkFilePath); apkFilePath.delete(); - DownloaderService.queue(this, urlString); + DownloaderService.queue(this, urlString, apk.repoId, urlString); } + 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)) { localBroadcastManager.unregisterReceiver(this); + } else if (Downloader.ACTION_CONNECTION_FAILED.equals(action)) { + DownloaderService.queue(context, urlString, 0, urlString); } else { throw new RuntimeException("intent action not handled!"); } } }; - DownloaderService.queue(this, obbUrlString); + DownloaderService.queue(this, obbUrlString, 0, obbUrlString); localBroadcastManager.registerReceiver(downloadReceiver, DownloaderService.getIntentFilter(obbUrlString)); } @@ -268,6 +275,8 @@ public class InstallManagerService extends Service { public void onReceive(Context context, Intent intent) { Uri downloadUri = intent.getData(); String urlString = downloadUri.toString(); + long repoId = intent.getLongExtra(Downloader.EXTRA_REPO_ID, 0); + String mirrorUrlString = intent.getStringExtra(Downloader.EXTRA_MIRROR_URL); switch (intent.getAction()) { case Downloader.ACTION_STARTED: @@ -287,7 +296,7 @@ public class InstallManagerService extends Service { File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH)); 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); localBroadcastManager.unregisterReceiver(this); @@ -303,6 +312,16 @@ public class InstallManagerService extends Service { appUpdateStatusManager.setDownloadError(urlString, intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE)); localBroadcastManager.unregisterReceiver(this); 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: throw new RuntimeException("intent action not handled!"); } diff --git a/app/src/main/java/org/fdroid/fdroid/net/Downloader.java b/app/src/main/java/org/fdroid/fdroid/net/Downloader.java index 1ef195bb7..885dc3d79 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/Downloader.java +++ b/app/src/main/java/org/fdroid/fdroid/net/Downloader.java @@ -8,6 +8,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.ConnectException; import java.net.URL; import java.util.Timer; 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_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_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 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_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_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 int bytesRead; @@ -36,6 +41,8 @@ public abstract class Downloader { String cacheTag; boolean notFound; + private volatile int timeout = 10000; + /** * For sending download progress, should only be called in {@link #progressTask} */ @@ -58,6 +65,14 @@ public abstract class Downloader { 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 * 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(); - 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) diff --git a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java index a234b6d1d..8ac4633ac 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java +++ b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java @@ -40,17 +40,19 @@ import org.fdroid.fdroid.installer.ApkCache; import java.io.File; import java.io.IOException; +import java.net.ConnectException; +import java.net.SocketTimeoutException; import java.net.URL; /** * DownloaderService is a service that handles asynchronous 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 * 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 - * operation, it will receive the queued {@link #queue(Context, String)} and - * {@link #cancel(Context, String)} requests again due to + * operation, it will receive the queued {@link #queue(Context, String, long, String)} + * and {@link #cancel(Context, String)} requests again due to * {@link Service#START_REDELIVER_INTENT}. Bad requests will be ignored, * including on restart after killing via {@link Service#START_NOT_STICKY}. *

@@ -86,6 +88,7 @@ public class DownloaderService extends Service { private static volatile ServiceHandler serviceHandler; private static volatile Downloader downloader; private LocalBroadcastManager localBroadcastManager; + private static volatile int timeout; private final class ServiceHandler extends Handler { ServiceHandler(Looper looper) { @@ -188,7 +191,9 @@ public class DownloaderService extends Service { private void handleIntent(Intent intent) { final Uri uri = intent.getData(); 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 { downloader = DownloaderFactory.create(this, uri, localFile); @@ -202,18 +207,22 @@ public class DownloaderService extends Service { localBroadcastManager.sendBroadcast(intent); } }); + downloader.setTimeout(timeout); downloader.download(); 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 { - sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile); + sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile, repoId, originalUrlString); } } 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) { e.printStackTrace(); sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile, - e.getLocalizedMessage()); + e.getLocalizedMessage(), repoId, originalUrlString); } finally { if (downloader != null) { downloader.close(); @@ -226,19 +235,26 @@ public class DownloaderService extends Service { sendBroadcast(uri, action, null, null); } - private void sendBroadcast(Uri uri, String action, File file) { - sendBroadcast(uri, action, file, null); + private void sendBroadcast(Uri uri, String action, File file, long repoId, String originalUrlString) { + sendBroadcast(uri, action, file, null, repoId, originalUrlString); } 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.setData(uri); + intent.setData(Uri.parse(originalUrlString)); if (file != null) { intent.putExtra(Downloader.EXTRA_DOWNLOAD_PATH, file.getAbsolutePath()); } if (!TextUtils.isEmpty(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); } @@ -251,7 +267,7 @@ public class DownloaderService extends Service { * @param urlString The URL to add to the download queue * @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)) { return; } @@ -259,6 +275,8 @@ public class DownloaderService extends Service { Intent intent = new Intent(context, DownloaderService.class); intent.setAction(ACTION_QUEUE); intent.setData(Uri.parse(urlString)); + intent.putExtra(Downloader.EXTRA_REPO_ID, repoId); + intent.putExtra(Downloader.EXTRA_CANONICAL_URL, originalUrlString); context.startService(intent); } @@ -269,7 +287,7 @@ public class DownloaderService extends Service { * * @param context this app's {@link Context} * @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) { if (TextUtils.isEmpty(urlString)) { @@ -304,6 +322,10 @@ public class DownloaderService extends Service { 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. * @@ -316,6 +338,7 @@ public class DownloaderService extends Service { intentFilter.addAction(Downloader.ACTION_PROGRESS); intentFilter.addAction(Downloader.ACTION_COMPLETE); intentFilter.addAction(Downloader.ACTION_INTERRUPTED); + intentFilter.addAction(Downloader.ACTION_CONNECTION_FAILED); intentFilter.addDataScheme(uri.getScheme()); intentFilter.addDataAuthority(uri.getHost(), String.valueOf(uri.getPort())); intentFilter.addDataPath(uri.getPath(), PatternMatcher.PATTERN_LITERAL); diff --git a/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java b/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java index 5a2818098..c44f94487 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java +++ b/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java @@ -14,8 +14,10 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.net.ConnectException; import java.net.HttpURLConnection; import java.net.MalformedURLException; +import java.net.SocketTimeoutException; import java.net.URL; public class HttpDownloader extends Downloader { @@ -77,7 +79,7 @@ public class HttpDownloader extends Downloader { * @see Cookieless cookies */ @Override - public void download() throws IOException, InterruptedException { + public void download() throws ConnectException, IOException, InterruptedException { // get the file size from the server HttpURLConnection tmpConn = getConnection(); tmpConn.setRequestMethod("HEAD"); @@ -126,7 +128,7 @@ public class HttpDownloader extends Downloader { && FDroidApp.subnetInfo.isInRange(host); // on the same subnet as we are } - private HttpURLConnection getConnection() throws IOException { + private HttpURLConnection getConnection() throws SocketTimeoutException, IOException { HttpURLConnection connection; if (isSwapUrl()) { // 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.setConnectTimeout(getTimeout()); if (username != null && password != null) { // add authorization header from username / password if set