Merge branch 'mirrors' into 'master'
Mirror support for the index and apps Closes #35 See merge request fdroid/fdroidclient!578
This commit is contained in:
commit
5e91bb7892
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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!");
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user