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.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);
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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<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.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!");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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}.
 | 
			
		||||
 * <p>
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -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 <a href="http://lucb1e.com/rp/cookielesscookies">Cookieless cookies</a>
 | 
			
		||||
     */
 | 
			
		||||
    @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
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user