Merge branch 'clean-up-repo-and-downloader' into 'master'
expose mirror options; Clean up Repo and Downloader See merge request fdroid/fdroidclient!663
This commit is contained in:
commit
5f2b053b1c
@ -180,7 +180,7 @@ def preDexEnabled = "true".equals(System.getProperty("pre-dex", "true"))
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 24
|
compileSdkVersion 24
|
||||||
buildToolsVersion '25.0.2'
|
buildToolsVersion '25.0.3'
|
||||||
useLibrary 'org.apache.http.legacy'
|
useLibrary 'org.apache.http.legacy'
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
@ -60,12 +60,8 @@ import org.fdroid.fdroid.installer.InstallHistoryService;
|
|||||||
import org.fdroid.fdroid.net.ImageLoaderForUIL;
|
import org.fdroid.fdroid.net.ImageLoaderForUIL;
|
||||||
import org.fdroid.fdroid.net.WifiStateChangeService;
|
import org.fdroid.fdroid.net.WifiStateChangeService;
|
||||||
import org.fdroid.fdroid.views.hiding.HidingManager;
|
import org.fdroid.fdroid.views.hiding.HidingManager;
|
||||||
import sun.net.www.protocol.bluetooth.Handler;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
|
||||||
import java.net.URLStreamHandler;
|
|
||||||
import java.net.URLStreamHandlerFactory;
|
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -370,16 +366,6 @@ public class FDroidApp extends Application {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// This is added so that the bluetooth:// scheme we use for URLs the BluetoothDownloader
|
|
||||||
// understands is not treated as invalid by the java.net.URL class. The actual Handler does
|
|
||||||
// nothing, but its presence is enough.
|
|
||||||
URL.setURLStreamHandlerFactory(new URLStreamHandlerFactory() {
|
|
||||||
@Override
|
|
||||||
public URLStreamHandler createURLStreamHandler(String protocol) {
|
|
||||||
return TextUtils.equals(protocol, "bluetooth") ? new Handler() : null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
final Context context = this;
|
final Context context = this;
|
||||||
Preferences.get().registerUnstableUpdatesChangeListener(new Preferences.ChangeListener() {
|
Preferences.get().registerUnstableUpdatesChangeListener(new Preferences.ChangeListener() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -6,7 +6,6 @@ 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;
|
||||||
@ -15,7 +14,6 @@ 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;
|
||||||
@ -31,7 +29,6 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.ConnectException;
|
import java.net.ConnectException;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
import java.net.URL;
|
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@ -135,7 +132,7 @@ public class IndexV1Updater extends RepoUpdater {
|
|||||||
if (downloader != null) {
|
if (downloader != null) {
|
||||||
FileUtils.deleteQuietly(downloader.outputFile);
|
FileUtils.deleteQuietly(downloader.outputFile);
|
||||||
}
|
}
|
||||||
throw new RepoUpdater.UpdateException(repo, "Error getting index file", e2);
|
throw new RepoUpdater.UpdateException("Error getting index file", e2);
|
||||||
} catch (InterruptedException e2) {
|
} catch (InterruptedException e2) {
|
||||||
// ignored if canceled, the local database just won't be updated
|
// ignored if canceled, the local database just won't be updated
|
||||||
}
|
}
|
||||||
@ -144,7 +141,7 @@ public class IndexV1Updater extends RepoUpdater {
|
|||||||
if (downloader != null) {
|
if (downloader != null) {
|
||||||
FileUtils.deleteQuietly(downloader.outputFile);
|
FileUtils.deleteQuietly(downloader.outputFile);
|
||||||
}
|
}
|
||||||
throw new RepoUpdater.UpdateException(repo, "Error getting index file", e);
|
throw new RepoUpdater.UpdateException("Error getting index file", e);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
// ignored if canceled, the local database just won't be updated
|
// ignored if canceled, the local database just won't be updated
|
||||||
}
|
}
|
||||||
@ -157,7 +154,7 @@ public class IndexV1Updater extends RepoUpdater {
|
|||||||
JarFile jarFile = new JarFile(outputFile, true);
|
JarFile jarFile = new JarFile(outputFile, true);
|
||||||
JarEntry indexEntry = (JarEntry) jarFile.getEntry(DATA_FILE_NAME);
|
JarEntry indexEntry = (JarEntry) jarFile.getEntry(DATA_FILE_NAME);
|
||||||
InputStream indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry),
|
InputStream indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry),
|
||||||
processIndexListener, new URL(repo.address), (int) indexEntry.getSize());
|
processIndexListener, repo.address, (int) indexEntry.getSize());
|
||||||
processIndexV1(indexInputStream, indexEntry, cacheTag);
|
processIndexV1(indexInputStream, indexEntry, cacheTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,7 +233,7 @@ public class IndexV1Updater extends RepoUpdater {
|
|||||||
long timestamp = (Long) repoMap.get("timestamp") / 1000;
|
long timestamp = (Long) repoMap.get("timestamp") / 1000;
|
||||||
|
|
||||||
if (repo.timestamp > timestamp) {
|
if (repo.timestamp > timestamp) {
|
||||||
throw new RepoUpdater.UpdateException(repo, "index.jar is older that current index! "
|
throw new RepoUpdater.UpdateException("index.jar is older that current index! "
|
||||||
+ timestamp + " < " + repo.timestamp);
|
+ timestamp + " < " + repo.timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -410,16 +407,14 @@ public class IndexV1Updater extends RepoUpdater {
|
|||||||
String certFromJar = Hasher.hex(rawCertFromJar);
|
String certFromJar = Hasher.hex(rawCertFromJar);
|
||||||
|
|
||||||
if (TextUtils.isEmpty(certFromJar)) {
|
if (TextUtils.isEmpty(certFromJar)) {
|
||||||
throw new SigningException(repo,
|
throw new SigningException(SIGNED_FILE_NAME + " must have an included signing certificate!");
|
||||||
SIGNED_FILE_NAME + " must have an included signing certificate!");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (repo.signingCertificate == null) {
|
if (repo.signingCertificate == null) {
|
||||||
if (repo.fingerprint != null) {
|
if (repo.fingerprint != null) {
|
||||||
String fingerprintFromJar = Utils.calcFingerprint(rawCertFromJar);
|
String fingerprintFromJar = Utils.calcFingerprint(rawCertFromJar);
|
||||||
if (!repo.fingerprint.equalsIgnoreCase(fingerprintFromJar)) {
|
if (!repo.fingerprint.equalsIgnoreCase(fingerprintFromJar)) {
|
||||||
throw new SigningException(repo,
|
throw new SigningException("Supplied certificate fingerprint does not match!");
|
||||||
"Supplied certificate fingerprint does not match!");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Utils.debugLog(TAG, "Saving new signing certificate to database for " + repo.address);
|
Utils.debugLog(TAG, "Saving new signing certificate to database for " + repo.address);
|
||||||
@ -431,14 +426,14 @@ public class IndexV1Updater extends RepoUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (TextUtils.isEmpty(repo.signingCertificate)) {
|
if (TextUtils.isEmpty(repo.signingCertificate)) {
|
||||||
throw new SigningException(repo, "A empty repo signing certificate is invalid!");
|
throw new SigningException("A empty repo signing certificate is invalid!");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (repo.signingCertificate.equals(certFromJar)) {
|
if (repo.signingCertificate.equals(certFromJar)) {
|
||||||
return; // we have a match!
|
return; // we have a match!
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new SigningException(repo, "Signing certificate does not match!");
|
throw new SigningException("Signing certificate does not match!");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,11 @@ package org.fdroid.fdroid;
|
|||||||
import java.io.BufferedInputStream;
|
import java.io.BufferedInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URL;
|
|
||||||
|
|
||||||
class ProgressBufferedInputStream extends BufferedInputStream {
|
class ProgressBufferedInputStream extends BufferedInputStream {
|
||||||
|
|
||||||
private final ProgressListener progressListener;
|
private final ProgressListener progressListener;
|
||||||
private final URL sourceUrl;
|
private final String urlString;
|
||||||
private final int totalBytes;
|
private final int totalBytes;
|
||||||
|
|
||||||
private int currentBytes;
|
private int currentBytes;
|
||||||
@ -17,10 +16,10 @@ class ProgressBufferedInputStream extends BufferedInputStream {
|
|||||||
* Reports progress to the specified {@link ProgressListener}, with the
|
* Reports progress to the specified {@link ProgressListener}, with the
|
||||||
* progress based on the {@code totalBytes}.
|
* progress based on the {@code totalBytes}.
|
||||||
*/
|
*/
|
||||||
ProgressBufferedInputStream(InputStream in, ProgressListener progressListener, URL sourceUrl, int totalBytes) {
|
ProgressBufferedInputStream(InputStream in, ProgressListener progressListener, String urlString, int totalBytes) {
|
||||||
super(in);
|
super(in);
|
||||||
this.progressListener = progressListener;
|
this.progressListener = progressListener;
|
||||||
this.sourceUrl = sourceUrl;
|
this.urlString = urlString;
|
||||||
this.totalBytes = totalBytes;
|
this.totalBytes = totalBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,7 +31,7 @@ class ProgressBufferedInputStream extends BufferedInputStream {
|
|||||||
* the digits changing because it looks pretty, < 9000 since the reads won't
|
* the digits changing because it looks pretty, < 9000 since the reads won't
|
||||||
* line up exactly */
|
* line up exactly */
|
||||||
if (currentBytes % 333333 < 9000) {
|
if (currentBytes % 333333 < 9000) {
|
||||||
progressListener.onProgress(sourceUrl, currentBytes, totalBytes);
|
progressListener.onProgress(urlString, currentBytes, totalBytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return super.read(buffer, byteOffset, byteCount);
|
return super.read(buffer, byteOffset, byteCount);
|
||||||
|
@ -19,6 +19,6 @@ import java.net.URL;
|
|||||||
*/
|
*/
|
||||||
public interface ProgressListener {
|
public interface ProgressListener {
|
||||||
|
|
||||||
void onProgress(URL sourceUrl, int bytesRead, int totalBytes);
|
void onProgress(String urlString, long bytesRead, long totalBytes);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,6 @@ import javax.xml.parsers.SAXParserFactory;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URL;
|
|
||||||
import java.security.CodeSigner;
|
import java.security.CodeSigner;
|
||||||
import java.security.cert.Certificate;
|
import java.security.cert.Certificate;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
@ -139,7 +138,7 @@ public class RepoUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new UpdateException(repo, "Error getting index file", e);
|
throw new UpdateException("Error getting index file", e);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
// ignored if canceled, the local database just won't be updated
|
// ignored if canceled, the local database just won't be updated
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
@ -202,7 +201,7 @@ public class RepoUpdater {
|
|||||||
InputStream indexInputStream = null;
|
InputStream indexInputStream = null;
|
||||||
try {
|
try {
|
||||||
if (downloadedFile == null || !downloadedFile.exists()) {
|
if (downloadedFile == null || !downloadedFile.exists()) {
|
||||||
throw new UpdateException(repo, downloadedFile + " does not exist!");
|
throw new UpdateException(downloadedFile + " does not exist!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Due to a bug in Android 5.0 Lollipop, the inclusion of spongycastle causes
|
// Due to a bug in Android 5.0 Lollipop, the inclusion of spongycastle causes
|
||||||
@ -213,7 +212,7 @@ public class RepoUpdater {
|
|||||||
JarFile jarFile = new JarFile(downloadedFile, true);
|
JarFile jarFile = new JarFile(downloadedFile, true);
|
||||||
JarEntry indexEntry = (JarEntry) jarFile.getEntry("index.xml");
|
JarEntry indexEntry = (JarEntry) jarFile.getEntry("index.xml");
|
||||||
indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry),
|
indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry),
|
||||||
processIndexListener, new URL(repo.address), (int) indexEntry.getSize());
|
processIndexListener, repo.address, (int) indexEntry.getSize());
|
||||||
|
|
||||||
// Process the index...
|
// Process the index...
|
||||||
SAXParserFactory factory = SAXParserFactory.newInstance();
|
SAXParserFactory factory = SAXParserFactory.newInstance();
|
||||||
@ -226,7 +225,7 @@ public class RepoUpdater {
|
|||||||
|
|
||||||
long timestamp = repoDetailsToSave.getAsLong(RepoTable.Cols.TIMESTAMP);
|
long timestamp = repoDetailsToSave.getAsLong(RepoTable.Cols.TIMESTAMP);
|
||||||
if (timestamp < repo.timestamp) {
|
if (timestamp < repo.timestamp) {
|
||||||
throw new UpdateException(repo, "index.jar is older that current index! "
|
throw new UpdateException("index.jar is older that current index! "
|
||||||
+ timestamp + " < " + repo.timestamp);
|
+ timestamp + " < " + repo.timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,7 +236,7 @@ public class RepoUpdater {
|
|||||||
assertSigningCertFromXmlCorrect();
|
assertSigningCertFromXmlCorrect();
|
||||||
commitToDb();
|
commitToDb();
|
||||||
} catch (SAXException | ParserConfigurationException | IOException e) {
|
} catch (SAXException | ParserConfigurationException | IOException e) {
|
||||||
throw new UpdateException(repo, "Error parsing index", e);
|
throw new UpdateException("Error parsing index", e);
|
||||||
} finally {
|
} finally {
|
||||||
FDroidApp.enableSpongyCastleOnLollipop();
|
FDroidApp.enableSpongyCastleOnLollipop();
|
||||||
Utils.closeQuietly(indexInputStream);
|
Utils.closeQuietly(indexInputStream);
|
||||||
@ -251,14 +250,14 @@ public class RepoUpdater {
|
|||||||
|
|
||||||
protected final ProgressListener downloadListener = new ProgressListener() {
|
protected final ProgressListener downloadListener = new ProgressListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onProgress(URL sourceUrl, int bytesRead, int totalBytes) {
|
public void onProgress(String urlString, long bytesRead, long totalBytes) {
|
||||||
UpdateService.reportDownloadProgress(context, RepoUpdater.this, bytesRead, totalBytes);
|
UpdateService.reportDownloadProgress(context, RepoUpdater.this, bytesRead, totalBytes);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
protected final ProgressListener processIndexListener = new ProgressListener() {
|
protected final ProgressListener processIndexListener = new ProgressListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onProgress(URL sourceUrl, int bytesRead, int totalBytes) {
|
public void onProgress(String urlString, long bytesRead, long totalBytes) {
|
||||||
UpdateService.reportProcessIndexProgress(context, RepoUpdater.this, bytesRead, totalBytes);
|
UpdateService.reportProcessIndexProgress(context, RepoUpdater.this, bytesRead, totalBytes);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -343,22 +342,19 @@ public class RepoUpdater {
|
|||||||
public static class UpdateException extends Exception {
|
public static class UpdateException extends Exception {
|
||||||
|
|
||||||
private static final long serialVersionUID = -4492452418826132803L;
|
private static final long serialVersionUID = -4492452418826132803L;
|
||||||
public final Repo repo;
|
|
||||||
|
|
||||||
public UpdateException(Repo repo, String message) {
|
public UpdateException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
this.repo = repo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public UpdateException(Repo repo, String message, Exception cause) {
|
public UpdateException(String message, Exception cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
this.repo = repo;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class SigningException extends UpdateException {
|
public static class SigningException extends UpdateException {
|
||||||
public SigningException(Repo repo, String message) {
|
public SigningException(String message) {
|
||||||
super(repo, "Repository was not signed correctly: " + message);
|
super("Repository was not signed correctly: " + message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -367,18 +363,18 @@ public class RepoUpdater {
|
|||||||
* signing setups that would be valid for a regular jar. This validates those
|
* signing setups that would be valid for a regular jar. This validates those
|
||||||
* restrictions.
|
* restrictions.
|
||||||
*/
|
*/
|
||||||
X509Certificate getSigningCertFromJar(JarEntry jarEntry) throws SigningException {
|
public static X509Certificate getSigningCertFromJar(JarEntry jarEntry) throws SigningException {
|
||||||
final CodeSigner[] codeSigners = jarEntry.getCodeSigners();
|
final CodeSigner[] codeSigners = jarEntry.getCodeSigners();
|
||||||
if (codeSigners == null || codeSigners.length == 0) {
|
if (codeSigners == null || codeSigners.length == 0) {
|
||||||
throw new SigningException(repo, "No signature found in index");
|
throw new SigningException("No signature found in index");
|
||||||
}
|
}
|
||||||
/* we could in theory support more than 1, but as of now we do not */
|
/* we could in theory support more than 1, but as of now we do not */
|
||||||
if (codeSigners.length > 1) {
|
if (codeSigners.length > 1) {
|
||||||
throw new SigningException(repo, "index.jar must be signed by a single code signer!");
|
throw new SigningException("index.jar must be signed by a single code signer!");
|
||||||
}
|
}
|
||||||
List<? extends Certificate> certs = codeSigners[0].getSignerCertPath().getCertificates();
|
List<? extends Certificate> certs = codeSigners[0].getSignerCertPath().getCertificates();
|
||||||
if (certs.size() != 1) {
|
if (certs.size() != 1) {
|
||||||
throw new SigningException(repo, "index.jar code signers must only have a single certificate!");
|
throw new SigningException("index.jar code signers must only have a single certificate!");
|
||||||
}
|
}
|
||||||
return (X509Certificate) certs.get(0);
|
return (X509Certificate) certs.get(0);
|
||||||
}
|
}
|
||||||
@ -404,7 +400,7 @@ public class RepoUpdater {
|
|||||||
String fingerprintFromJar = Utils.calcFingerprint(rawCertFromJar);
|
String fingerprintFromJar = Utils.calcFingerprint(rawCertFromJar);
|
||||||
if (!repo.fingerprint.equalsIgnoreCase(fingerprintFromIndexXml)
|
if (!repo.fingerprint.equalsIgnoreCase(fingerprintFromIndexXml)
|
||||||
|| !repo.fingerprint.equalsIgnoreCase(fingerprintFromJar)) {
|
|| !repo.fingerprint.equalsIgnoreCase(fingerprintFromJar)) {
|
||||||
throw new SigningException(repo, "Supplied certificate fingerprint does not match!");
|
throw new SigningException("Supplied certificate fingerprint does not match!");
|
||||||
}
|
}
|
||||||
} // else - no info to check things are valid, so just Trust On First Use
|
} // else - no info to check things are valid, so just Trust On First Use
|
||||||
|
|
||||||
@ -435,7 +431,7 @@ public class RepoUpdater {
|
|||||||
if (TextUtils.isEmpty(repo.signingCertificate)
|
if (TextUtils.isEmpty(repo.signingCertificate)
|
||||||
|| TextUtils.isEmpty(certFromJar)
|
|| TextUtils.isEmpty(certFromJar)
|
||||||
|| TextUtils.isEmpty(certFromIndexXml)) {
|
|| TextUtils.isEmpty(certFromIndexXml)) {
|
||||||
throw new SigningException(repo, "A empty repo or signing certificate is invalid!");
|
throw new SigningException("A empty repo or signing certificate is invalid!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// though its called repo.signingCertificate, its actually a X509 certificate
|
// though its called repo.signingCertificate, its actually a X509 certificate
|
||||||
@ -444,7 +440,7 @@ public class RepoUpdater {
|
|||||||
&& certFromIndexXml.equals(certFromJar)) {
|
&& certFromIndexXml.equals(certFromJar)) {
|
||||||
return; // we have a match!
|
return; // we have a match!
|
||||||
}
|
}
|
||||||
throw new SigningException(repo, "Signing certificate does not match!");
|
throw new SigningException("Signing certificate does not match!");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -516,12 +516,13 @@ public class UpdateService extends IntentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void reportDownloadProgress(Context context, RepoUpdater updater, int bytesRead, int totalBytes) {
|
public static void reportDownloadProgress(Context context, RepoUpdater updater,
|
||||||
|
long bytesRead, long totalBytes) {
|
||||||
Utils.debugLog(TAG, "Downloading " + updater.indexUrl + "(" + bytesRead + "/" + totalBytes + ")");
|
Utils.debugLog(TAG, "Downloading " + updater.indexUrl + "(" + bytesRead + "/" + totalBytes + ")");
|
||||||
String downloadedSizeFriendly = Utils.getFriendlySize(bytesRead);
|
String downloadedSizeFriendly = Utils.getFriendlySize(bytesRead);
|
||||||
int percent = -1;
|
int percent = -1;
|
||||||
if (totalBytes > 0) {
|
if (totalBytes > 0) {
|
||||||
percent = (int) ((double) bytesRead / totalBytes * 100);
|
percent = (int) (bytesRead / (totalBytes * 100L));
|
||||||
}
|
}
|
||||||
String message;
|
String message;
|
||||||
if (totalBytes == -1) {
|
if (totalBytes == -1) {
|
||||||
@ -534,13 +535,14 @@ public class UpdateService extends IntentService {
|
|||||||
sendStatus(context, STATUS_INFO, message, percent);
|
sendStatus(context, STATUS_INFO, message, percent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void reportProcessIndexProgress(Context context, RepoUpdater updater, int bytesRead, int totalBytes) {
|
public static void reportProcessIndexProgress(Context context, RepoUpdater updater,
|
||||||
|
long bytesRead, long totalBytes) {
|
||||||
Utils.debugLog(TAG, "Processing " + updater.indexUrl + "(" + bytesRead + "/" + totalBytes + ")");
|
Utils.debugLog(TAG, "Processing " + updater.indexUrl + "(" + bytesRead + "/" + totalBytes + ")");
|
||||||
String downloadedSize = Utils.getFriendlySize(bytesRead);
|
String downloadedSize = Utils.getFriendlySize(bytesRead);
|
||||||
String totalSize = Utils.getFriendlySize(totalBytes);
|
String totalSize = Utils.getFriendlySize(totalBytes);
|
||||||
int percent = -1;
|
int percent = -1;
|
||||||
if (totalBytes > 0) {
|
if (totalBytes > 0) {
|
||||||
percent = (int) ((double) bytesRead / totalBytes * 100);
|
percent = (int) (bytesRead / (totalBytes * 100L));
|
||||||
}
|
}
|
||||||
String message = context.getString(R.string.status_processing_xml_percent, updater.indexUrl, downloadedSize, totalSize, percent);
|
String message = context.getString(R.string.status_processing_xml_percent, updater.indexUrl, downloadedSize, totalSize, percent);
|
||||||
sendStatus(context, STATUS_INFO, message, percent);
|
sendStatus(context, STATUS_INFO, message, percent);
|
||||||
|
@ -88,6 +88,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
|||||||
+ RepoTable.Cols.TIMESTAMP + " integer not null default 0, "
|
+ RepoTable.Cols.TIMESTAMP + " integer not null default 0, "
|
||||||
+ RepoTable.Cols.ICON + " string, "
|
+ RepoTable.Cols.ICON + " string, "
|
||||||
+ RepoTable.Cols.MIRRORS + " string, "
|
+ RepoTable.Cols.MIRRORS + " string, "
|
||||||
|
+ RepoTable.Cols.USER_MIRRORS + " string, "
|
||||||
+ RepoTable.Cols.PUSH_REQUESTS + " integer not null default " + Repo.PUSH_REQUEST_IGNORE
|
+ RepoTable.Cols.PUSH_REQUESTS + " integer not null default " + Repo.PUSH_REQUEST_IGNORE
|
||||||
+ ");";
|
+ ");";
|
||||||
|
|
||||||
@ -214,7 +215,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
|||||||
+ "primary key(" + ApkAntiFeatureJoinTable.Cols.APK_ID + ", " + ApkAntiFeatureJoinTable.Cols.ANTI_FEATURE_ID + ") "
|
+ "primary key(" + ApkAntiFeatureJoinTable.Cols.APK_ID + ", " + ApkAntiFeatureJoinTable.Cols.ANTI_FEATURE_ID + ") "
|
||||||
+ " );";
|
+ " );";
|
||||||
|
|
||||||
protected static final int DB_VERSION = 77;
|
protected static final int DB_VERSION = 78;
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
@ -321,6 +322,17 @@ public class DBHelper extends SQLiteOpenHelper {
|
|||||||
addApkAntiFeatures(db, oldVersion);
|
addApkAntiFeatures(db, oldVersion);
|
||||||
addIgnoreVulnPref(db, oldVersion);
|
addIgnoreVulnPref(db, oldVersion);
|
||||||
addLiberapayID(db, oldVersion);
|
addLiberapayID(db, oldVersion);
|
||||||
|
addUserMirrorsFields(db, oldVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addUserMirrorsFields(SQLiteDatabase db, int oldVersion) {
|
||||||
|
if (oldVersion >= 78) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!columnExists(db, RepoTable.NAME, RepoTable.Cols.USER_MIRRORS)) {
|
||||||
|
Utils.debugLog(TAG, "Adding " + RepoTable.Cols.USER_MIRRORS + " field to " + RepoTable.NAME + " table in db.");
|
||||||
|
db.execSQL("alter table " + RepoTable.NAME + " add column " + RepoTable.Cols.USER_MIRRORS + " string;");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addLiberapayID(SQLiteDatabase db, int oldVersion) {
|
private void addLiberapayID(SQLiteDatabase db, int oldVersion) {
|
||||||
@ -581,7 +593,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
|||||||
updateRepoPriority(db, gpPubKey, gpArchiveAddress, 4);
|
updateRepoPriority(db, gpPubKey, gpArchiveAddress, 4);
|
||||||
|
|
||||||
int priority = 5;
|
int priority = 5;
|
||||||
String[] projection = new String[] {RepoTable.Cols.SIGNING_CERT, RepoTable.Cols.ADDRESS};
|
String[] projection = new String[]{RepoTable.Cols.SIGNING_CERT, RepoTable.Cols.ADDRESS};
|
||||||
|
|
||||||
// Order by ID, because that is a good analogy for the order in which they were added.
|
// Order by ID, because that is a good analogy for the order in which they were added.
|
||||||
// The order in which they were added is likely the order they present in the ManageRepos activity.
|
// The order in which they were added is likely the order they present in the ManageRepos activity.
|
||||||
@ -606,7 +618,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
|||||||
RepoTable.NAME,
|
RepoTable.NAME,
|
||||||
values,
|
values,
|
||||||
RepoTable.Cols.SIGNING_CERT + " = ? AND " + RepoTable.Cols.ADDRESS + " = ?",
|
RepoTable.Cols.SIGNING_CERT + " = ? AND " + RepoTable.Cols.ADDRESS + " = ?",
|
||||||
new String[] {signingCert, address}
|
new String[]{signingCert, address}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -687,7 +699,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
|||||||
|
|
||||||
db.execSQL(createTableDdl);
|
db.execSQL(createTableDdl);
|
||||||
|
|
||||||
String nonPackageNameFields = TextUtils.join(", ", new String[] {
|
String nonPackageNameFields = TextUtils.join(", ", new String[]{
|
||||||
ApkTable.Cols.APP_ID,
|
ApkTable.Cols.APP_ID,
|
||||||
ApkTable.Cols.VERSION_NAME,
|
ApkTable.Cols.VERSION_NAME,
|
||||||
ApkTable.Cols.REPO_ID,
|
ApkTable.Cols.REPO_ID,
|
||||||
@ -766,7 +778,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
|||||||
}
|
}
|
||||||
List<Repo> oldrepos = new ArrayList<>();
|
List<Repo> oldrepos = new ArrayList<>();
|
||||||
Cursor cursor = db.query(RepoTable.NAME,
|
Cursor cursor = db.query(RepoTable.NAME,
|
||||||
new String[] {RepoTable.Cols.ADDRESS, RepoTable.Cols.IN_USE, RepoTable.Cols.SIGNING_CERT},
|
new String[]{RepoTable.Cols.ADDRESS, RepoTable.Cols.IN_USE, RepoTable.Cols.SIGNING_CERT},
|
||||||
null, null, null, null, null);
|
null, null, null, null, null);
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
if (cursor.getCount() > 0) {
|
if (cursor.getCount() > 0) {
|
||||||
@ -847,7 +859,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
|||||||
}
|
}
|
||||||
List<Repo> oldrepos = new ArrayList<>();
|
List<Repo> oldrepos = new ArrayList<>();
|
||||||
Cursor cursor = db.query(RepoTable.NAME,
|
Cursor cursor = db.query(RepoTable.NAME,
|
||||||
new String[] {RepoTable.Cols.ADDRESS, RepoTable.Cols.SIGNING_CERT},
|
new String[]{RepoTable.Cols.ADDRESS, RepoTable.Cols.SIGNING_CERT},
|
||||||
null, null, null, null, null);
|
null, null, null, null, null);
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
if (cursor.getCount() > 0) {
|
if (cursor.getCount() > 0) {
|
||||||
@ -865,7 +877,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
|||||||
for (final Repo repo : oldrepos) {
|
for (final Repo repo : oldrepos) {
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
values.put(RepoTable.Cols.FINGERPRINT, Utils.calcFingerprint(repo.signingCertificate));
|
values.put(RepoTable.Cols.FINGERPRINT, Utils.calcFingerprint(repo.signingCertificate));
|
||||||
db.update(RepoTable.NAME, values, RepoTable.Cols.ADDRESS + " = ?", new String[] {repo.address});
|
db.update(RepoTable.NAME, values, RepoTable.Cols.ADDRESS + " = ?", new String[]{repo.address});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -954,7 +966,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
|||||||
|
|
||||||
db.execSQL(createTableDdl);
|
db.execSQL(createTableDdl);
|
||||||
|
|
||||||
String nonIdFields = TextUtils.join(", ", new String[] {
|
String nonIdFields = TextUtils.join(", ", new String[]{
|
||||||
RepoTable.Cols.ADDRESS,
|
RepoTable.Cols.ADDRESS,
|
||||||
RepoTable.Cols.NAME,
|
RepoTable.Cols.NAME,
|
||||||
RepoTable.Cols.DESCRIPTION,
|
RepoTable.Cols.DESCRIPTION,
|
||||||
@ -1235,8 +1247,8 @@ public class DBHelper extends SQLiteOpenHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean tableExists(SQLiteDatabase db, String table) {
|
private static boolean tableExists(SQLiteDatabase db, String table) {
|
||||||
Cursor cursor = db.query("sqlite_master", new String[] {"name"},
|
Cursor cursor = db.query("sqlite_master", new String[]{"name"},
|
||||||
"type = 'table' AND name = ?", new String[] {table}, null, null, null);
|
"type = 'table' AND name = ?", new String[]{table}, null, null, null);
|
||||||
|
|
||||||
boolean exists = cursor.getCount() > 0;
|
boolean exists = cursor.getCount() > 0;
|
||||||
cursor.close();
|
cursor.close();
|
||||||
|
@ -4,7 +4,6 @@ import android.content.Context;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import org.fdroid.fdroid.R;
|
import org.fdroid.fdroid.R;
|
||||||
import org.fdroid.fdroid.Utils;
|
import org.fdroid.fdroid.Utils;
|
||||||
import org.fdroid.fdroid.localrepo.peers.WifiPeer;
|
import org.fdroid.fdroid.localrepo.peers.WifiPeer;
|
||||||
@ -82,17 +81,12 @@ public class NewRepoConfig {
|
|||||||
scheme = scheme.toLowerCase(Locale.ENGLISH);
|
scheme = scheme.toLowerCase(Locale.ENGLISH);
|
||||||
host = host.toLowerCase(Locale.ENGLISH);
|
host = host.toLowerCase(Locale.ENGLISH);
|
||||||
|
|
||||||
// We only listen for /fdroid/archive or /fdroid/repo paths when receiving a HTTP(S) intent.
|
if (uri.getPath() == null
|
||||||
// For fdroidrepo(s) intents, we are less picky and will accept any path.
|
|| !Arrays.asList("https", "http", "fdroidrepos", "fdroidrepo").contains(scheme)) {
|
||||||
boolean isHttpScheme = TextUtils.equals("http", scheme) || TextUtils.equals("https", scheme);
|
|
||||||
String path = uri.getPath();
|
|
||||||
if (path == null || isHttpScheme && !(path.contains("/fdroid/archive") || path.contains("/fdroid/repo"))) {
|
|
||||||
isValidRepo = false;
|
isValidRepo = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isFdroidScheme = TextUtils.equals("fdroidrepo", scheme) || TextUtils.equals("fdroidrepos", scheme);
|
|
||||||
|
|
||||||
String userInfo = uri.getUserInfo();
|
String userInfo = uri.getUserInfo();
|
||||||
if (userInfo != null) {
|
if (userInfo != null) {
|
||||||
String[] userInfoTokens = userInfo.split(":");
|
String[] userInfoTokens = userInfo.split(":");
|
||||||
@ -109,15 +103,8 @@ public class NewRepoConfig {
|
|||||||
bssid = uri.getQueryParameter("bssid");
|
bssid = uri.getQueryParameter("bssid");
|
||||||
ssid = uri.getQueryParameter("ssid");
|
ssid = uri.getQueryParameter("ssid");
|
||||||
fromSwap = uri.getQueryParameter("swap") != null;
|
fromSwap = uri.getQueryParameter("swap") != null;
|
||||||
|
|
||||||
if (!isFdroidScheme && !isHttpScheme) {
|
|
||||||
isValidRepo = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uriString = sanitizeRepoUri(uri);
|
uriString = sanitizeRepoUri(uri);
|
||||||
isValidRepo = true;
|
isValidRepo = true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getBssid() {
|
public String getBssid() {
|
||||||
@ -175,7 +162,9 @@ public class NewRepoConfig {
|
|||||||
return errorMessage;
|
return errorMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sanitize and format an incoming repo URI for function and readability */
|
/**
|
||||||
|
* Sanitize and format an incoming repo URI for function and readability
|
||||||
|
*/
|
||||||
public static String sanitizeRepoUri(Uri uri) {
|
public static String sanitizeRepoUri(Uri uri) {
|
||||||
String scheme = uri.getScheme();
|
String scheme = uri.getScheme();
|
||||||
String host = uri.getHost();
|
String host = uri.getHost();
|
||||||
|
@ -26,13 +26,13 @@ package org.fdroid.fdroid.data;
|
|||||||
import android.content.ContentValues;
|
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.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.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@ -94,6 +94,11 @@ public class Repo extends ValueObject {
|
|||||||
/** Official mirrors of this repo, considered automatically interchangeable */
|
/** Official mirrors of this repo, considered automatically interchangeable */
|
||||||
public String[] mirrors;
|
public String[] mirrors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrors added by the user, either by UI input or by attaching removeable storage
|
||||||
|
*/
|
||||||
|
public String[] userMirrors;
|
||||||
|
|
||||||
/** How to treat push requests included in this repo's index XML */
|
/** How to treat push requests included in this repo's index XML */
|
||||||
public int pushRequests = PUSH_REQUEST_IGNORE;
|
public int pushRequests = PUSH_REQUEST_IGNORE;
|
||||||
|
|
||||||
@ -160,6 +165,9 @@ public class Repo extends ValueObject {
|
|||||||
case Cols.MIRRORS:
|
case Cols.MIRRORS:
|
||||||
mirrors = Utils.parseCommaSeparatedString(cursor.getString(i));
|
mirrors = Utils.parseCommaSeparatedString(cursor.getString(i));
|
||||||
break;
|
break;
|
||||||
|
case Cols.USER_MIRRORS:
|
||||||
|
userMirrors = Utils.parseCommaSeparatedString(cursor.getString(i));
|
||||||
|
break;
|
||||||
case Cols.PUSH_REQUESTS:
|
case Cols.PUSH_REQUESTS:
|
||||||
pushRequests = cursor.getInt(i);
|
pushRequests = cursor.getInt(i);
|
||||||
break;
|
break;
|
||||||
@ -297,19 +305,37 @@ public class Repo extends ValueObject {
|
|||||||
mirrors = Utils.parseCommaSeparatedString(values.getAsString(Cols.MIRRORS));
|
mirrors = Utils.parseCommaSeparatedString(values.getAsString(Cols.MIRRORS));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (values.containsKey(Cols.USER_MIRRORS)) {
|
||||||
|
userMirrors = Utils.parseCommaSeparatedString(values.getAsString(Cols.USER_MIRRORS));
|
||||||
|
}
|
||||||
|
|
||||||
if (values.containsKey(Cols.PUSH_REQUESTS)) {
|
if (values.containsKey(Cols.PUSH_REQUESTS)) {
|
||||||
pushRequests = toInt(values.getAsInteger(Cols.PUSH_REQUESTS));
|
pushRequests = toInt(values.getAsInteger(Cols.PUSH_REQUESTS));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasMirrors() {
|
public boolean hasMirrors() {
|
||||||
return mirrors != null && mirrors.length > 1;
|
return (mirrors != null && mirrors.length > 1)
|
||||||
|
|| (userMirrors != null && userMirrors.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> getMirrorList() {
|
||||||
|
final ArrayList<String> allMirrors = new ArrayList<String>();
|
||||||
|
if (userMirrors != null) {
|
||||||
|
allMirrors.addAll(Arrays.asList(userMirrors));
|
||||||
|
}
|
||||||
|
if (mirrors != null) {
|
||||||
|
allMirrors.addAll(Arrays.asList(mirrors));
|
||||||
|
}
|
||||||
|
return allMirrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of available mirrors, including the canonical repo.
|
||||||
|
*/
|
||||||
public int getMirrorCount() {
|
public int getMirrorCount() {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
if (mirrors != null && mirrors.length > 1) {
|
for (String m : getMirrorList()) {
|
||||||
for (String m: mirrors) {
|
|
||||||
if (!m.equals(address)) {
|
if (!m.equals(address)) {
|
||||||
if (FDroidApp.isUsingTor()) {
|
if (FDroidApp.isUsingTor()) {
|
||||||
count++;
|
count++;
|
||||||
@ -320,7 +346,6 @@ public class Repo extends ValueObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,7 +353,7 @@ public class Repo extends ValueObject {
|
|||||||
if (TextUtils.isEmpty(lastWorkingMirror)) {
|
if (TextUtils.isEmpty(lastWorkingMirror)) {
|
||||||
lastWorkingMirror = address;
|
lastWorkingMirror = address;
|
||||||
}
|
}
|
||||||
List<String> shuffledMirrors = Arrays.asList(mirrors);
|
List<String> shuffledMirrors = getMirrorList();
|
||||||
Collections.shuffle(shuffledMirrors);
|
Collections.shuffle(shuffledMirrors);
|
||||||
if (shuffledMirrors.size() > 1) {
|
if (shuffledMirrors.size() > 1) {
|
||||||
for (String m : shuffledMirrors) {
|
for (String m : shuffledMirrors) {
|
||||||
|
@ -106,7 +106,7 @@ public class RepoPersister {
|
|||||||
try {
|
try {
|
||||||
context.getContentResolver().applyBatch(TempApkProvider.getAuthority(), apkOperations);
|
context.getContentResolver().applyBatch(TempApkProvider.getAuthority(), apkOperations);
|
||||||
} catch (RemoteException | OperationApplicationException e) {
|
} catch (RemoteException | OperationApplicationException e) {
|
||||||
throw new RepoUpdater.UpdateException(repo, "An internal error occurred while updating the database", e);
|
throw new RepoUpdater.UpdateException("An internal error occurred while updating the database", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,7 +122,7 @@ public class RepoPersister {
|
|||||||
context.getContentResolver().applyBatch(TempAppProvider.getAuthority(), appOperations);
|
context.getContentResolver().applyBatch(TempAppProvider.getAuthority(), appOperations);
|
||||||
return getIdsForPackages(appsToSave);
|
return getIdsForPackages(appsToSave);
|
||||||
} catch (RemoteException | OperationApplicationException e) {
|
} catch (RemoteException | OperationApplicationException e) {
|
||||||
throw new RepoUpdater.UpdateException(repo, "An internal error occurred while updating the database", e);
|
throw new RepoUpdater.UpdateException("An internal error occurred while updating the database", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -362,12 +362,13 @@ public interface Schema {
|
|||||||
String TIMESTAMP = "timestamp";
|
String TIMESTAMP = "timestamp";
|
||||||
String ICON = "icon";
|
String ICON = "icon";
|
||||||
String MIRRORS = "mirrors";
|
String MIRRORS = "mirrors";
|
||||||
|
String USER_MIRRORS = "userMirrors";
|
||||||
String PUSH_REQUESTS = "pushRequests";
|
String PUSH_REQUESTS = "pushRequests";
|
||||||
|
|
||||||
String[] ALL = {
|
String[] ALL = {
|
||||||
_ID, ADDRESS, NAME, DESCRIPTION, IN_USE, PRIORITY, SIGNING_CERT,
|
_ID, ADDRESS, NAME, DESCRIPTION, IN_USE, PRIORITY, SIGNING_CERT,
|
||||||
FINGERPRINT, MAX_AGE, LAST_UPDATED, LAST_ETAG, VERSION, IS_SWAP,
|
FINGERPRINT, MAX_AGE, LAST_UPDATED, LAST_ETAG, VERSION, IS_SWAP,
|
||||||
USERNAME, PASSWORD, TIMESTAMP, ICON, MIRRORS, PUSH_REQUESTS,
|
USERNAME, PASSWORD, TIMESTAMP, ICON, MIRRORS, USER_MIRRORS, PUSH_REQUESTS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package org.fdroid.fdroid.net;
|
package org.fdroid.fdroid.net;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.apache.commons.io.input.BoundedInputStream;
|
import org.apache.commons.io.input.BoundedInputStream;
|
||||||
import org.fdroid.fdroid.Utils;
|
import org.fdroid.fdroid.Utils;
|
||||||
import org.fdroid.fdroid.net.bluetooth.BluetoothClient;
|
import org.fdroid.fdroid.net.bluetooth.BluetoothClient;
|
||||||
@ -14,7 +14,6 @@ import org.fdroid.fdroid.net.bluetooth.httpish.Response;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URL;
|
|
||||||
|
|
||||||
public class BluetoothDownloader extends Downloader {
|
public class BluetoothDownloader extends Downloader {
|
||||||
|
|
||||||
@ -24,10 +23,11 @@ public class BluetoothDownloader extends Downloader {
|
|||||||
private FileDetails fileDetails;
|
private FileDetails fileDetails;
|
||||||
private final String sourcePath;
|
private final String sourcePath;
|
||||||
|
|
||||||
public BluetoothDownloader(String macAddress, URL sourceUrl, File destFile) throws IOException {
|
public BluetoothDownloader(Uri uri, File destFile) throws IOException {
|
||||||
super(sourceUrl, destFile);
|
super(uri, destFile);
|
||||||
|
String macAddress = uri.getHost().replace("-", ":");
|
||||||
this.connection = new BluetoothClient(macAddress).openConnection();
|
this.connection = new BluetoothClient(macAddress).openConnection();
|
||||||
this.sourcePath = sourceUrl.getPath();
|
this.sourcePath = uri.getPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -58,7 +58,7 @@ public class BluetoothDownloader extends Downloader {
|
|||||||
if (fileDetails == null) {
|
if (fileDetails == null) {
|
||||||
Utils.debugLog(TAG, "Going to Bluetooth \"server\" to get file details.");
|
Utils.debugLog(TAG, "Going to Bluetooth \"server\" to get file details.");
|
||||||
try {
|
try {
|
||||||
fileDetails = Request.createHEAD(sourceUrl.getPath(), connection).send().toFileDetails();
|
fileDetails = Request.createHEAD(sourcePath, connection).send().toFileDetails();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(TAG, "Error getting file details from Bluetooth \"server\"", e);
|
Log.e(TAG, "Error getting file details from Bluetooth \"server\"", e);
|
||||||
}
|
}
|
||||||
@ -73,7 +73,7 @@ public class BluetoothDownloader extends Downloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int totalDownloadSize() {
|
public long totalDownloadSize() {
|
||||||
FileDetails details = getFileDetails();
|
FileDetails details = getFileDetails();
|
||||||
return details != null ? details.getFileSize() : -1;
|
return details != null ? details.getFileSize() : -1;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.fdroid.fdroid.net;
|
package org.fdroid.fdroid.net;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import org.fdroid.fdroid.ProgressListener;
|
import org.fdroid.fdroid.ProgressListener;
|
||||||
import org.fdroid.fdroid.Utils;
|
import org.fdroid.fdroid.Utils;
|
||||||
|
|
||||||
@ -9,7 +10,6 @@ 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.ConnectException;
|
||||||
import java.net.URL;
|
|
||||||
import java.util.Timer;
|
import java.util.Timer;
|
||||||
import java.util.TimerTask;
|
import java.util.TimerTask;
|
||||||
|
|
||||||
@ -32,12 +32,12 @@ public abstract class Downloader {
|
|||||||
public static final String EXTRA_MIRROR_URL = "org.fdroid.fdroid.net.Downloader.extra.ERROR_MIRROR_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 long bytesRead;
|
||||||
private volatile int totalBytes;
|
private volatile long totalBytes;
|
||||||
|
|
||||||
public final File outputFile;
|
public final File outputFile;
|
||||||
|
|
||||||
final URL sourceUrl;
|
final String urlString;
|
||||||
String cacheTag;
|
String cacheTag;
|
||||||
boolean notFound;
|
boolean notFound;
|
||||||
|
|
||||||
@ -52,8 +52,8 @@ public abstract class Downloader {
|
|||||||
|
|
||||||
protected abstract void close();
|
protected abstract void close();
|
||||||
|
|
||||||
Downloader(URL url, File destFile) {
|
Downloader(Uri uri, File destFile) {
|
||||||
this.sourceUrl = url;
|
this.urlString = uri.toString();
|
||||||
outputFile = destFile;
|
outputFile = destFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ public abstract class Downloader {
|
|||||||
|
|
||||||
public abstract boolean hasChanged();
|
public abstract boolean hasChanged();
|
||||||
|
|
||||||
protected abstract int totalDownloadSize();
|
protected abstract long totalDownloadSize();
|
||||||
|
|
||||||
public abstract void download() throws ConnectException, IOException, InterruptedException;
|
public abstract void download() throws ConnectException, IOException, InterruptedException;
|
||||||
|
|
||||||
@ -201,7 +201,7 @@ public abstract class Downloader {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (downloaderProgressListener != null) {
|
if (downloaderProgressListener != null) {
|
||||||
downloaderProgressListener.onProgress(sourceUrl, bytesRead, totalBytes);
|
downloaderProgressListener.onProgress(urlString, bytesRead, totalBytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -2,19 +2,15 @@ package org.fdroid.fdroid.net;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.support.v4.content.LocalBroadcastManager;
|
|
||||||
import org.fdroid.fdroid.data.Repo;
|
import org.fdroid.fdroid.data.Repo;
|
||||||
import org.fdroid.fdroid.data.RepoProvider;
|
import org.fdroid.fdroid.data.RepoProvider;
|
||||||
import org.fdroid.fdroid.data.Schema;
|
import org.fdroid.fdroid.data.Schema;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
|
||||||
|
|
||||||
public class DownloaderFactory {
|
public class DownloaderFactory {
|
||||||
|
|
||||||
private static LocalBroadcastManager localBroadcastManager;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads to a temporary file, which *you must delete yourself when
|
* Downloads to a temporary file, which *you must delete yourself when
|
||||||
* you are done. It is stored in {@link Context#getCacheDir()} and starts
|
* you are done. It is stored in {@link Context#getCacheDir()} and starts
|
||||||
@ -34,22 +30,19 @@ public class DownloaderFactory {
|
|||||||
|
|
||||||
public static Downloader create(Context context, String urlString, File destFile)
|
public static Downloader create(Context context, String urlString, File destFile)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
URL url = new URL(urlString);
|
|
||||||
Downloader downloader;
|
Downloader downloader;
|
||||||
if (localBroadcastManager == null) {
|
|
||||||
localBroadcastManager = LocalBroadcastManager.getInstance(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("bluetooth".equalsIgnoreCase(url.getProtocol())) {
|
Uri uri = Uri.parse(urlString);
|
||||||
String macAddress = url.getHost().replace("-", ":");
|
String scheme = uri.getScheme();
|
||||||
downloader = new BluetoothDownloader(macAddress, url, destFile);
|
if ("bluetooth".equals(scheme)) {
|
||||||
|
downloader = new BluetoothDownloader(uri, destFile);
|
||||||
} else {
|
} else {
|
||||||
final String[] projection = {Schema.RepoTable.Cols.USERNAME, Schema.RepoTable.Cols.PASSWORD};
|
final String[] projection = {Schema.RepoTable.Cols.USERNAME, Schema.RepoTable.Cols.PASSWORD};
|
||||||
Repo repo = RepoProvider.Helper.findByUrl(context, Uri.parse(url.toString()), projection);
|
Repo repo = RepoProvider.Helper.findByUrl(context, uri, projection);
|
||||||
if (repo == null) {
|
if (repo == null) {
|
||||||
downloader = new HttpDownloader(url, destFile);
|
downloader = new HttpDownloader(uri, destFile);
|
||||||
} else {
|
} else {
|
||||||
downloader = new HttpDownloader(url, destFile, repo.username, repo.password);
|
downloader = new HttpDownloader(uri, destFile, repo.username, repo.password);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return downloader;
|
return downloader;
|
||||||
|
@ -31,7 +31,6 @@ import android.os.PatternMatcher;
|
|||||||
import android.os.Process;
|
import android.os.Process;
|
||||||
import android.support.v4.content.LocalBroadcastManager;
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import org.fdroid.fdroid.ProgressListener;
|
import org.fdroid.fdroid.ProgressListener;
|
||||||
import org.fdroid.fdroid.R;
|
import org.fdroid.fdroid.R;
|
||||||
import org.fdroid.fdroid.Utils;
|
import org.fdroid.fdroid.Utils;
|
||||||
@ -42,7 +41,6 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.ConnectException;
|
import java.net.ConnectException;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
import java.net.URL;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DownloaderService is a service that handles asynchronous download requests
|
* DownloaderService is a service that handles asynchronous download requests
|
||||||
@ -199,7 +197,7 @@ public class DownloaderService extends Service {
|
|||||||
downloader = DownloaderFactory.create(this, uri, localFile);
|
downloader = DownloaderFactory.create(this, uri, localFile);
|
||||||
downloader.setListener(new ProgressListener() {
|
downloader.setListener(new ProgressListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onProgress(URL sourceUrl, int bytesRead, int totalBytes) {
|
public void onProgress(String urlString, long bytesRead, long totalBytes) {
|
||||||
Intent intent = new Intent(Downloader.ACTION_PROGRESS);
|
Intent intent = new Intent(Downloader.ACTION_PROGRESS);
|
||||||
intent.setData(uri);
|
intent.setData(uri);
|
||||||
intent.putExtra(Downloader.EXTRA_BYTES_READ, bytesRead);
|
intent.putExtra(Downloader.EXTRA_BYTES_READ, bytesRead);
|
||||||
@ -321,7 +319,7 @@ public class DownloaderService extends Service {
|
|||||||
* Check if a URL is actively being downloaded.
|
* Check if a URL is actively being downloaded.
|
||||||
*/
|
*/
|
||||||
private static boolean isActive(String urlString) {
|
private static boolean isActive(String urlString) {
|
||||||
return downloader != null && TextUtils.equals(urlString, downloader.sourceUrl.toString());
|
return downloader != null && TextUtils.equals(urlString, downloader.urlString);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setTimeout(int ms) {
|
public static void setTimeout(int ms) {
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package org.fdroid.fdroid.net;
|
package org.fdroid.fdroid.net;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
|
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
|
||||||
import info.guardianproject.netcipher.NetCipher;
|
import info.guardianproject.netcipher.NetCipher;
|
||||||
@ -27,29 +30,30 @@ public class HttpDownloader extends Downloader {
|
|||||||
|
|
||||||
private final String username;
|
private final String username;
|
||||||
private final String password;
|
private final String password;
|
||||||
|
private URL sourceUrl;
|
||||||
private HttpURLConnection connection;
|
private HttpURLConnection connection;
|
||||||
private boolean newFileAvailableOnServer;
|
private boolean newFileAvailableOnServer;
|
||||||
|
|
||||||
HttpDownloader(URL url, File destFile)
|
HttpDownloader(Uri uri, File destFile)
|
||||||
throws FileNotFoundException, MalformedURLException {
|
throws FileNotFoundException, MalformedURLException {
|
||||||
this(url, destFile, null, null);
|
this(uri, destFile, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a downloader that can authenticate via HTTP Basic Auth using the supplied
|
* Create a downloader that can authenticate via HTTP Basic Auth using the supplied
|
||||||
* {@code username} and {@code password}.
|
* {@code username} and {@code password}.
|
||||||
*
|
*
|
||||||
* @param url The file to download
|
* @param uri The file to download
|
||||||
* @param destFile Where the download is saved
|
* @param destFile Where the download is saved
|
||||||
* @param username Username for HTTP Basic Auth, use {@code null} to ignore
|
* @param username Username for HTTP Basic Auth, use {@code null} to ignore
|
||||||
* @param password Password for HTTP Basic Auth, use {@code null} to ignore
|
* @param password Password for HTTP Basic Auth, use {@code null} to ignore
|
||||||
* @throws FileNotFoundException
|
* @throws FileNotFoundException
|
||||||
* @throws MalformedURLException
|
* @throws MalformedURLException
|
||||||
*/
|
*/
|
||||||
HttpDownloader(URL url, File destFile, String username, String password)
|
HttpDownloader(Uri uri, File destFile, String username, String password)
|
||||||
throws FileNotFoundException, MalformedURLException {
|
throws FileNotFoundException, MalformedURLException {
|
||||||
super(url, destFile);
|
super(uri, destFile);
|
||||||
|
this.sourceUrl = new URL(urlString);
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
}
|
}
|
||||||
@ -93,7 +97,7 @@ public class HttpDownloader extends Downloader {
|
|||||||
case 200:
|
case 200:
|
||||||
contentLength = tmpConn.getContentLength();
|
contentLength = tmpConn.getContentLength();
|
||||||
if (!TextUtils.isEmpty(etag) && etag.equals(cacheTag)) {
|
if (!TextUtils.isEmpty(etag) && etag.equals(cacheTag)) {
|
||||||
Utils.debugLog(TAG, sourceUrl + " is cached, not downloading");
|
Utils.debugLog(TAG, urlString + " is cached, not downloading");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
newFileAvailableOnServer = true;
|
newFileAvailableOnServer = true;
|
||||||
@ -102,7 +106,7 @@ public class HttpDownloader extends Downloader {
|
|||||||
notFound = true;
|
notFound = true;
|
||||||
return;
|
return;
|
||||||
default:
|
default:
|
||||||
Utils.debugLog(TAG, "HEAD check of " + sourceUrl + " returned " + statusCode + ": "
|
Utils.debugLog(TAG, "HEAD check of " + urlString + " returned " + statusCode + ": "
|
||||||
+ tmpConn.getResponseMessage());
|
+ tmpConn.getResponseMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,7 +120,7 @@ public class HttpDownloader extends Downloader {
|
|||||||
resumable = true;
|
resumable = true;
|
||||||
}
|
}
|
||||||
setupConnection(resumable);
|
setupConnection(resumable);
|
||||||
Utils.debugLog(TAG, "downloading " + sourceUrl + " (is resumable: " + resumable + ")");
|
Utils.debugLog(TAG, "downloading " + urlString + " (is resumable: " + resumable + ")");
|
||||||
downloadFromStream(8192, resumable);
|
downloadFromStream(8192, resumable);
|
||||||
cacheTag = connection.getHeaderField(HEADER_FIELD_ETAG);
|
cacheTag = connection.getHeaderField(HEADER_FIELD_ETAG);
|
||||||
}
|
}
|
||||||
@ -169,8 +173,13 @@ public class HttpDownloader extends Downloader {
|
|||||||
// because as the repo grows, the tradeoff will
|
// because as the repo grows, the tradeoff will
|
||||||
// become more worth it.
|
// become more worth it.
|
||||||
@Override
|
@Override
|
||||||
public int totalDownloadSize() {
|
@TargetApi(24)
|
||||||
|
public long totalDownloadSize() {
|
||||||
|
if (Build.VERSION.SDK_INT < 24) {
|
||||||
return connection.getContentLength();
|
return connection.getContentLength();
|
||||||
|
} else {
|
||||||
|
return connection.getContentLengthLong();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -3,13 +3,13 @@ package org.fdroid.fdroid.net.bluetooth;
|
|||||||
public class FileDetails {
|
public class FileDetails {
|
||||||
|
|
||||||
private String cacheTag;
|
private String cacheTag;
|
||||||
private int fileSize;
|
private long fileSize;
|
||||||
|
|
||||||
public String getCacheTag() {
|
public String getCacheTag() {
|
||||||
return cacheTag;
|
return cacheTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getFileSize() {
|
public long getFileSize() {
|
||||||
return fileSize;
|
return fileSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
package org.fdroid.fdroid.views;
|
package org.fdroid.fdroid.views;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
@ -61,22 +62,25 @@ import org.fdroid.fdroid.data.Repo;
|
|||||||
import org.fdroid.fdroid.data.RepoProvider;
|
import org.fdroid.fdroid.data.RepoProvider;
|
||||||
import org.fdroid.fdroid.data.Schema.RepoTable;
|
import org.fdroid.fdroid.data.Schema.RepoTable;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
@SuppressWarnings("LineLength")
|
public class ManageReposActivity extends AppCompatActivity
|
||||||
public class ManageReposActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor>, RepoAdapter.EnabledListener {
|
implements LoaderManager.LoaderCallbacks<Cursor>, RepoAdapter.EnabledListener {
|
||||||
private static final String TAG = "ManageReposActivity";
|
private static final String TAG = "ManageReposActivity";
|
||||||
|
|
||||||
private static final String DEFAULT_NEW_REPO_TEXT = "https://";
|
private static final String DEFAULT_NEW_REPO_TEXT = "https://";
|
||||||
|
|
||||||
private enum AddRepoState {
|
private enum AddRepoState {
|
||||||
DOESNT_EXIST, EXISTS_FINGERPRINT_MISMATCH, EXISTS_FINGERPRINT_MATCH,
|
DOESNT_EXIST, EXISTS_FINGERPRINT_MISMATCH, EXISTS_ADD_MIRROR,
|
||||||
EXISTS_DISABLED, EXISTS_ENABLED, EXISTS_UPGRADABLE_TO_SIGNED, INVALID_URL,
|
EXISTS_DISABLED, EXISTS_ENABLED, EXISTS_UPGRADABLE_TO_SIGNED, INVALID_URL,
|
||||||
IS_SWAP
|
IS_SWAP
|
||||||
}
|
}
|
||||||
@ -213,18 +217,35 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
|
|||||||
private class AddRepo {
|
private class AddRepo {
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
private final HashMap<String, Repo> urlRepoMap = new HashMap<>();
|
||||||
|
private final HashMap<String, Repo> fingerprintRepoMap = new HashMap<>();
|
||||||
private final AlertDialog addRepoDialog;
|
private final AlertDialog addRepoDialog;
|
||||||
|
|
||||||
private final TextView overwriteMessage;
|
private final TextView overwriteMessage;
|
||||||
private final ColorStateList defaultTextColour;
|
private final ColorStateList defaultTextColour;
|
||||||
private final Button addButton;
|
private final Button addButton;
|
||||||
|
|
||||||
private AddRepoState addRepoState;
|
private AddRepoState addRepoState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new instance, setup GUI, and build maps for quickly looking
|
||||||
|
* up repos based on URL or fingerprint. These need to be in maps
|
||||||
|
* since the user input is validated as they are typing. This also
|
||||||
|
* checks that the repo type matches, e.g. "repo" or "archive".
|
||||||
|
*/
|
||||||
AddRepo(String newAddress, String newFingerprint, final String username, final String password) {
|
AddRepo(String newAddress, String newFingerprint, final String username, final String password) {
|
||||||
|
|
||||||
context = ManageReposActivity.this;
|
context = ManageReposActivity.this;
|
||||||
|
|
||||||
|
for (Repo repo : RepoProvider.Helper.all(context)) {
|
||||||
|
urlRepoMap.put(repo.address, repo);
|
||||||
|
for (String url : repo.getMirrorList()) {
|
||||||
|
urlRepoMap.put(url, repo);
|
||||||
|
}
|
||||||
|
if (TextUtils.equals(getRepoType(newAddress), getRepoType(repo.address))) {
|
||||||
|
fingerprintRepoMap.put(repo.fingerprint, repo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final View view = getLayoutInflater().inflate(R.layout.addrepo, null);
|
final View view = getLayoutInflater().inflate(R.layout.addrepo, null);
|
||||||
addRepoDialog = new AlertDialog.Builder(context).setView(view).create();
|
addRepoDialog = new AlertDialog.Builder(context).setView(view).create();
|
||||||
final EditText uriEditText = (EditText) view.findViewById(R.id.edit_uri);
|
final EditText uriEditText = (EditText) view.findViewById(R.id.edit_uri);
|
||||||
@ -275,7 +296,7 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
|
|||||||
try {
|
try {
|
||||||
url = normalizeUrl(url);
|
url = normalizeUrl(url);
|
||||||
} catch (URISyntaxException e) {
|
} catch (URISyntaxException e) {
|
||||||
invalidUrl();
|
invalidUrl(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,7 +310,8 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case IS_SWAP:
|
case IS_SWAP:
|
||||||
Utils.debugLog(TAG, "Removing existing swap repo " + url + " before adding new repo.");
|
Utils.debugLog(TAG, "Removing existing swap repo " + url
|
||||||
|
+ " before adding new repo.");
|
||||||
Repo repo = RepoProvider.Helper.findByAddress(context, url);
|
Repo repo = RepoProvider.Helper.findByAddress(context, url);
|
||||||
RepoProvider.Helper.remove(context, repo.getId());
|
RepoProvider.Helper.remove(context, repo.getId());
|
||||||
prepareToCreateNewRepo(url, fp, username, password);
|
prepareToCreateNewRepo(url, fp, username, password);
|
||||||
@ -297,7 +319,7 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
|
|||||||
|
|
||||||
case EXISTS_DISABLED:
|
case EXISTS_DISABLED:
|
||||||
case EXISTS_UPGRADABLE_TO_SIGNED:
|
case EXISTS_UPGRADABLE_TO_SIGNED:
|
||||||
case EXISTS_FINGERPRINT_MATCH:
|
case EXISTS_ADD_MIRROR:
|
||||||
updateAndEnableExistingRepo(url, fp);
|
updateAndEnableExistingRepo(url, fp);
|
||||||
finishedAddingRepo();
|
finishedAddingRepo();
|
||||||
break;
|
break;
|
||||||
@ -347,9 +369,31 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
|
|||||||
validateRepoDetails(newAddress == null ? "" : newAddress, newFingerprint == null ? "" : newFingerprint);
|
validateRepoDetails(newAddress == null ? "" : newAddress, newFingerprint == null ? "" : newFingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the repo type as represented by the final segment of the path. This is
|
||||||
|
* a bit trickier with {@code content://} URLs, since they might have
|
||||||
|
* encoded "/" chars in it, for example:
|
||||||
|
* {@code content://authority/tree/313E-1F1C%3A/document/313E-1F1C%3Aguardianproject.info%2Ffdroid%2Frepo}
|
||||||
|
*/
|
||||||
|
private String getRepoType(String url) {
|
||||||
|
String last = Uri.parse(url).getLastPathSegment();
|
||||||
|
if (last == null) {
|
||||||
|
return "";
|
||||||
|
} else {
|
||||||
|
return new File(last).getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare the repo and the fingerprint against existing repositories, to see if this
|
* Compare the repo and the fingerprint against existing repositories, to see if this
|
||||||
* repo matches and display a relevant message to the user if that is the case.
|
* repo matches and display a relevant message to the user if that is the case. There
|
||||||
|
* are many different cases to handle:
|
||||||
|
* <ul>
|
||||||
|
* <li> a signed repo with a {@link Repo#address URL} and fingerprint that matches
|
||||||
|
* <li> a signed repo with a matching fingerprint and URL that matches a mirror
|
||||||
|
* <li> a signed repo with a matching fingerprint, but the URL doesn't match any known mirror
|
||||||
|
* <li>an unsigned repo and no fingerprint was supplied
|
||||||
|
* </ul>
|
||||||
*/
|
*/
|
||||||
private void validateRepoDetails(@NonNull String uri, @NonNull String fingerprint) {
|
private void validateRepoDetails(@NonNull String uri, @NonNull String fingerprint) {
|
||||||
|
|
||||||
@ -361,72 +405,84 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
|
|||||||
// to the user until they try to save the repo.
|
// to the user until they try to save the repo.
|
||||||
}
|
}
|
||||||
|
|
||||||
final Repo repo = !TextUtils.isEmpty(uri) ? RepoProvider.Helper.findByAddress(context, uri) : null;
|
Repo repo = fingerprintRepoMap.get(fingerprint);
|
||||||
|
if (repo == null) {
|
||||||
|
repo = urlRepoMap.get(uri);
|
||||||
|
}
|
||||||
|
|
||||||
if (repo == null) {
|
if (repo == null) {
|
||||||
repoDoesntExist();
|
repoDoesntExist(repo);
|
||||||
} else {
|
} else {
|
||||||
if (repo.isSwap) {
|
if (repo.isSwap) {
|
||||||
repoIsSwap();
|
repoIsSwap(repo);
|
||||||
} else if (repo.fingerprint == null && fingerprint.length() > 0) {
|
} else if (repo.fingerprint == null && fingerprint.length() > 0) {
|
||||||
upgradingToSigned();
|
upgradingToSigned(repo);
|
||||||
} else if (repo.fingerprint != null && !repo.fingerprint.equalsIgnoreCase(fingerprint)) {
|
} else if (repo.fingerprint != null && !repo.fingerprint.equalsIgnoreCase(fingerprint)) {
|
||||||
repoFingerprintDoesntMatch();
|
repoFingerprintDoesntMatch(repo);
|
||||||
} else {
|
} else {
|
||||||
// Could be either an unsigned repo, and no fingerprint was supplied,
|
if (!TextUtils.equals(repo.address, uri)
|
||||||
// or it could be a signed repo with a matching fingerprint.
|
&& !repo.getMirrorList().contains(uri)) {
|
||||||
if (repo.inuse) {
|
repoExistsAddMirror(repo);
|
||||||
repoExistsAndEnabled();
|
} else if (repo.inuse) {
|
||||||
|
repoExistsAndEnabled(repo);
|
||||||
} else {
|
} else {
|
||||||
repoExistsAndDisabled();
|
repoExistsAndDisabled(repo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void repoDoesntExist() {
|
private void repoDoesntExist(Repo repo) {
|
||||||
updateUi(AddRepoState.DOESNT_EXIST, 0, false, R.string.repo_add_add, true);
|
updateUi(repo, AddRepoState.DOESNT_EXIST, 0, false, R.string.repo_add_add, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void repoIsSwap() {
|
private void repoIsSwap(Repo repo) {
|
||||||
updateUi(AddRepoState.IS_SWAP, 0, false, R.string.repo_add_add, true);
|
updateUi(repo, AddRepoState.IS_SWAP, 0, false, R.string.repo_add_add, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same address with different fingerprint, this could be malicious, so display a message
|
* Same address with different fingerprint, this could be malicious, so display a message
|
||||||
* force the user to manually delete the repo before adding this one.
|
* force the user to manually delete the repo before adding this one.
|
||||||
*/
|
*/
|
||||||
private void repoFingerprintDoesntMatch() {
|
private void repoFingerprintDoesntMatch(Repo repo) {
|
||||||
updateUi(AddRepoState.EXISTS_FINGERPRINT_MISMATCH, R.string.repo_delete_to_overwrite,
|
updateUi(repo, AddRepoState.EXISTS_FINGERPRINT_MISMATCH,
|
||||||
|
R.string.repo_delete_to_overwrite,
|
||||||
true, R.string.overwrite, false);
|
true, R.string.overwrite, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void invalidUrl() {
|
private void invalidUrl(Repo repo) {
|
||||||
updateUi(AddRepoState.INVALID_URL, R.string.invalid_url, true,
|
updateUi(repo, AddRepoState.INVALID_URL, R.string.invalid_url, true,
|
||||||
R.string.repo_add_add, false);
|
R.string.repo_add_add, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void repoExistsAndDisabled() {
|
private void repoExistsAndDisabled(Repo repo) {
|
||||||
updateUi(AddRepoState.EXISTS_DISABLED,
|
updateUi(repo, AddRepoState.EXISTS_DISABLED,
|
||||||
R.string.repo_exists_enable, false, R.string.enable, true);
|
R.string.repo_exists_enable, false, R.string.enable, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void repoExistsAndEnabled() {
|
private void repoExistsAndEnabled(Repo repo) {
|
||||||
updateUi(AddRepoState.EXISTS_ENABLED, R.string.repo_exists_and_enabled, false,
|
updateUi(repo, AddRepoState.EXISTS_ENABLED, R.string.repo_exists_and_enabled, false,
|
||||||
R.string.ok, true);
|
R.string.ok, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void upgradingToSigned() {
|
private void repoExistsAddMirror(Repo repo) {
|
||||||
updateUi(AddRepoState.EXISTS_UPGRADABLE_TO_SIGNED, R.string.repo_exists_add_fingerprint,
|
updateUi(repo, AddRepoState.EXISTS_ADD_MIRROR, R.string.repo_exists_add_mirror, false,
|
||||||
|
R.string.repo_add_mirror, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void upgradingToSigned(Repo repo) {
|
||||||
|
updateUi(repo, AddRepoState.EXISTS_UPGRADABLE_TO_SIGNED, R.string.repo_exists_add_fingerprint,
|
||||||
false, R.string.add_key, true);
|
false, R.string.add_key, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateUi(AddRepoState state, int messageRes, boolean redMessage, int addTextRes, boolean addEnabled) {
|
@DebugLog
|
||||||
|
private void updateUi(Repo repo, AddRepoState state, int messageRes, boolean redMessage, int addTextRes,
|
||||||
|
boolean addEnabled) {
|
||||||
if (addRepoState != state) {
|
if (addRepoState != state) {
|
||||||
addRepoState = state;
|
addRepoState = state;
|
||||||
|
|
||||||
if (messageRes > 0) {
|
if (messageRes > 0) {
|
||||||
overwriteMessage.setText(messageRes);
|
overwriteMessage.setText(String.format(getString(messageRes), repo.name));
|
||||||
overwriteMessage.setVisibility(View.VISIBLE);
|
overwriteMessage.setVisibility(View.VISIBLE);
|
||||||
if (redMessage) {
|
if (redMessage) {
|
||||||
overwriteMessage.setTextColor(getResources().getColor(R.color.red));
|
overwriteMessage.setTextColor(getResources().getColor(R.color.red));
|
||||||
@ -445,30 +501,50 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
|
|||||||
/**
|
/**
|
||||||
* Adds a new repo to the database.
|
* Adds a new repo to the database.
|
||||||
*/
|
*/
|
||||||
private void prepareToCreateNewRepo(final String originalAddress, final String fingerprint, final String username, final String password) {
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
private void prepareToCreateNewRepo(final String originalAddress, final String fingerprint,
|
||||||
|
final String username, final String password) {
|
||||||
|
|
||||||
addRepoDialog.findViewById(R.id.add_repo_form).setVisibility(View.GONE);
|
final View addRepoForm = addRepoDialog.findViewById(R.id.add_repo_form);
|
||||||
addRepoDialog.getButton(AlertDialog.BUTTON_POSITIVE).setVisibility(View.GONE);
|
addRepoForm.setVisibility(View.GONE);
|
||||||
|
final View positiveButton = addRepoDialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||||
|
positiveButton.setVisibility(View.GONE);
|
||||||
|
|
||||||
final TextView textSearching = (TextView) addRepoDialog.findViewById(R.id.text_searching_for_repo);
|
final TextView textSearching = (TextView) addRepoDialog.findViewById(R.id.text_searching_for_repo);
|
||||||
textSearching.setText(getString(R.string.repo_searching_address, originalAddress));
|
textSearching.setText(getString(R.string.repo_searching_address, originalAddress));
|
||||||
|
|
||||||
|
final Button skip = addRepoDialog.getButton(AlertDialog.BUTTON_NEGATIVE);
|
||||||
|
skip.setText(R.string.skip);
|
||||||
|
|
||||||
final AsyncTask<String, String, String> checker = new AsyncTask<String, String, String>() {
|
final AsyncTask<String, String, String> checker = new AsyncTask<String, String, String>() {
|
||||||
|
|
||||||
private int statusCode = -1;
|
private int statusCode = -1;
|
||||||
|
private final static int REFRESH_DIALOG = Integer.MAX_VALUE;
|
||||||
|
private final static int HTTP_UNAUTHORIZED = 401;
|
||||||
|
private final static int HTTP_OK = 200;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String doInBackground(String... params) {
|
protected String doInBackground(String... params) {
|
||||||
|
|
||||||
final String originalAddress = params[0];
|
final String originalAddress = params[0];
|
||||||
|
|
||||||
|
if (fingerprintRepoMap.containsKey(fingerprint)) {
|
||||||
|
statusCode = REFRESH_DIALOG;
|
||||||
|
return originalAddress;
|
||||||
|
}
|
||||||
|
|
||||||
final String[] pathsToCheck = {"", "fdroid/repo", "repo"};
|
final String[] pathsToCheck = {"", "fdroid/repo", "repo"};
|
||||||
for (final String path : pathsToCheck) {
|
for (final String path : pathsToCheck) {
|
||||||
|
|
||||||
Utils.debugLog(TAG, "Checking for repo at " + originalAddress + " with suffix \"" + path + "\".");
|
Utils.debugLog(TAG, "Check for repo at " + originalAddress + " with suffix '" + path + "'");
|
||||||
Uri.Builder builder = Uri.parse(originalAddress).buildUpon().appendEncodedPath(path);
|
Uri.Builder builder = Uri.parse(originalAddress).buildUpon().appendEncodedPath(path);
|
||||||
final String addressWithoutIndex = builder.build().toString();
|
final String addressWithoutIndex = builder.build().toString();
|
||||||
publishProgress(addressWithoutIndex);
|
publishProgress(addressWithoutIndex);
|
||||||
|
|
||||||
|
if (urlRepoMap.containsKey(addressWithoutIndex)) {
|
||||||
|
statusCode = REFRESH_DIALOG;
|
||||||
|
return addressWithoutIndex;
|
||||||
|
}
|
||||||
|
|
||||||
final Uri uri = builder.appendPath("index.jar").build();
|
final Uri uri = builder.appendPath("index.jar").build();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -482,7 +558,7 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isCancelled()) {
|
if (isCancelled()) {
|
||||||
Utils.debugLog(TAG, "Not checking any more repo addresses, because process was skipped.");
|
Utils.debugLog(TAG, "Not checking more repo addresses, because process was skipped.");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -491,14 +567,13 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean checkForRepository(Uri indexUri) throws IOException {
|
private boolean checkForRepository(Uri indexUri) throws IOException {
|
||||||
|
|
||||||
final URL url = new URL(indexUri.toString());
|
final URL url = new URL(indexUri.toString());
|
||||||
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
connection.setRequestMethod("HEAD");
|
connection.setRequestMethod("HEAD");
|
||||||
|
|
||||||
statusCode = connection.getResponseCode();
|
statusCode = connection.getResponseCode();
|
||||||
|
|
||||||
return statusCode == 401 || statusCode == 200;
|
return statusCode == HTTP_UNAUTHORIZED || statusCode == HTTP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -512,10 +587,11 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
|
|||||||
|
|
||||||
if (addRepoDialog.isShowing()) {
|
if (addRepoDialog.isShowing()) {
|
||||||
|
|
||||||
if (statusCode == 401) {
|
if (statusCode == HTTP_UNAUTHORIZED) {
|
||||||
|
|
||||||
final View view = getLayoutInflater().inflate(R.layout.login, null);
|
final View view = getLayoutInflater().inflate(R.layout.login, null);
|
||||||
final AlertDialog credentialsDialog = new AlertDialog.Builder(context).setView(view).create();
|
final AlertDialog credentialsDialog = new AlertDialog.Builder(context)
|
||||||
|
.setView(view).create();
|
||||||
final EditText nameInput = (EditText) view.findViewById(R.id.edit_name);
|
final EditText nameInput = (EditText) view.findViewById(R.id.edit_name);
|
||||||
final EditText passwordInput = (EditText) view.findViewById(R.id.edit_password);
|
final EditText passwordInput = (EditText) view.findViewById(R.id.edit_password);
|
||||||
|
|
||||||
@ -543,12 +619,21 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
|
|||||||
new DialogInterface.OnClickListener() {
|
new DialogInterface.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
createNewRepo(newAddress, fingerprint, nameInput.getText().toString(), passwordInput.getText().toString());
|
createNewRepo(newAddress, fingerprint,
|
||||||
|
nameInput.getText().toString(),
|
||||||
|
passwordInput.getText().toString());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
credentialsDialog.show();
|
credentialsDialog.show();
|
||||||
|
|
||||||
|
} else if (statusCode == REFRESH_DIALOG) {
|
||||||
|
addRepoForm.setVisibility(View.VISIBLE);
|
||||||
|
positiveButton.setVisibility(View.VISIBLE);
|
||||||
|
textSearching.setText("");
|
||||||
|
skip.setText(R.string.cancel);
|
||||||
|
skip.setOnClickListener(null);
|
||||||
|
validateRepoDetails(newAddress, fingerprint);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// create repo without username/password
|
// create repo without username/password
|
||||||
@ -558,8 +643,6 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Button skip = addRepoDialog.getButton(AlertDialog.BUTTON_NEGATIVE);
|
|
||||||
skip.setText(R.string.skip);
|
|
||||||
skip.setOnClickListener(new View.OnClickListener() {
|
skip.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
@ -615,7 +698,8 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
|
|||||||
createNewRepo(address, fingerprint, null, null);
|
createNewRepo(address, fingerprint, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createNewRepo(String address, String fingerprint, final String username, final String password) {
|
private void createNewRepo(String address, String fingerprint,
|
||||||
|
final String username, final String password) {
|
||||||
try {
|
try {
|
||||||
address = normalizeUrl(address);
|
address = normalizeUrl(address);
|
||||||
} catch (URISyntaxException e) {
|
} catch (URISyntaxException e) {
|
||||||
@ -634,7 +718,7 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
|
|||||||
|
|
||||||
RepoProvider.Helper.insert(context, values);
|
RepoProvider.Helper.insert(context, values);
|
||||||
finishedAddingRepo();
|
finishedAddingRepo();
|
||||||
Toast.makeText(ManageReposActivity.this, getString(R.string.repo_added, address), Toast.LENGTH_SHORT).show();
|
Toast.makeText(context, getString(R.string.repo_added, address), Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -651,11 +735,34 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
|
|||||||
}
|
}
|
||||||
|
|
||||||
Utils.debugLog(TAG, "Enabling existing repo: " + url);
|
Utils.debugLog(TAG, "Enabling existing repo: " + url);
|
||||||
Repo repo = RepoProvider.Helper.findByAddress(context, url);
|
Repo repo = fingerprintRepoMap.get(fingerprint);
|
||||||
|
if (repo == null) {
|
||||||
|
repo = RepoProvider.Helper.findByAddress(context, url);
|
||||||
|
}
|
||||||
|
|
||||||
ContentValues values = new ContentValues(2);
|
ContentValues values = new ContentValues(2);
|
||||||
values.put(RepoTable.Cols.IN_USE, 1);
|
values.put(RepoTable.Cols.IN_USE, 1);
|
||||||
values.put(RepoTable.Cols.FINGERPRINT, fingerprint);
|
values.put(RepoTable.Cols.FINGERPRINT, fingerprint);
|
||||||
|
if (!TextUtils.equals(url, repo.address)) {
|
||||||
|
boolean addUserMirror = true;
|
||||||
|
for (String mirror : repo.getMirrorList()) {
|
||||||
|
if (TextUtils.equals(mirror, url)) {
|
||||||
|
addUserMirror = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (addUserMirror) {
|
||||||
|
if (repo.userMirrors == null) {
|
||||||
|
repo.userMirrors = new String[]{url};
|
||||||
|
} else {
|
||||||
|
int last = repo.userMirrors.length;
|
||||||
|
repo.userMirrors = Arrays.copyOf(repo.userMirrors, last);
|
||||||
|
repo.userMirrors[last] = url;
|
||||||
|
}
|
||||||
|
values.put(RepoTable.Cols.USER_MIRRORS, Utils.serializeCommaSeparatedString(repo.userMirrors));
|
||||||
|
}
|
||||||
|
}
|
||||||
RepoProvider.Helper.update(context, repo, values);
|
RepoProvider.Helper.update(context, repo, values);
|
||||||
|
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
finishedAddingRepo();
|
finishedAddingRepo();
|
||||||
}
|
}
|
||||||
@ -675,7 +782,6 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
|
|||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addRepoFromIntent(Intent intent) {
|
private void addRepoFromIntent(Intent intent) {
|
||||||
@ -683,7 +789,8 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
|
|||||||
NewRepoConfig newRepoConfig = new NewRepoConfig(this, intent);
|
NewRepoConfig newRepoConfig = new NewRepoConfig(this, intent);
|
||||||
if (newRepoConfig.isValidRepo()) {
|
if (newRepoConfig.isValidRepo()) {
|
||||||
isImportingRepo = true;
|
isImportingRepo = true;
|
||||||
showAddRepo(newRepoConfig.getRepoUriString(), newRepoConfig.getFingerprint(), newRepoConfig.getUsername(), newRepoConfig.getPassword());
|
showAddRepo(newRepoConfig.getRepoUriString(), newRepoConfig.getFingerprint(),
|
||||||
|
newRepoConfig.getUsername(), newRepoConfig.getPassword());
|
||||||
checkIfNewRepoOnSameWifi(newRepoConfig);
|
checkIfNewRepoOnSameWifi(newRepoConfig);
|
||||||
} else if (newRepoConfig.getErrorMessage() != null) {
|
} else if (newRepoConfig.getErrorMessage() != null) {
|
||||||
Toast.makeText(this, newRepoConfig.getErrorMessage(), Toast.LENGTH_LONG).show();
|
Toast.makeText(this, newRepoConfig.getErrorMessage(), Toast.LENGTH_LONG).show();
|
||||||
|
@ -108,12 +108,19 @@ public class RepoDetailsActivity extends ActionBarActivity {
|
|||||||
RepoTable.Cols.NAME,
|
RepoTable.Cols.NAME,
|
||||||
RepoTable.Cols.ADDRESS,
|
RepoTable.Cols.ADDRESS,
|
||||||
RepoTable.Cols.FINGERPRINT,
|
RepoTable.Cols.FINGERPRINT,
|
||||||
|
RepoTable.Cols.MIRRORS,
|
||||||
|
RepoTable.Cols.USER_MIRRORS,
|
||||||
};
|
};
|
||||||
repo = RepoProvider.Helper.findById(this, repoId, projection);
|
repo = RepoProvider.Helper.findById(this, repoId, projection);
|
||||||
|
|
||||||
TextView inputUrl = (TextView) findViewById(R.id.input_repo_url);
|
TextView inputUrl = (TextView) findViewById(R.id.input_repo_url);
|
||||||
inputUrl.setText(repo.address);
|
inputUrl.setText(repo.address);
|
||||||
|
|
||||||
|
if (repo.address.startsWith("content://")) {
|
||||||
|
// no need to show a QR Code, it is not shareable
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Uri uri = Uri.parse(repo.address);
|
Uri uri = Uri.parse(repo.address);
|
||||||
uri = uri.buildUpon().appendQueryParameter("fingerprint", repo.fingerprint).build();
|
uri = uri.buildUpon().appendQueryParameter("fingerprint", repo.fingerprint).build();
|
||||||
String qrUriString = uri.toString();
|
String qrUriString = uri.toString();
|
||||||
@ -321,6 +328,33 @@ public class RepoDetailsActivity extends ActionBarActivity {
|
|||||||
TextView numApps = (TextView) repoView.findViewById(R.id.text_num_apps);
|
TextView numApps = (TextView) repoView.findViewById(R.id.text_num_apps);
|
||||||
TextView lastUpdated = (TextView) repoView.findViewById(R.id.text_last_update);
|
TextView lastUpdated = (TextView) repoView.findViewById(R.id.text_last_update);
|
||||||
|
|
||||||
|
if (repo.mirrors != null) {
|
||||||
|
TextView officialMirrorsLabel = (TextView) repoView.findViewById(R.id.label_official_mirrors);
|
||||||
|
officialMirrorsLabel.setVisibility(View.VISIBLE);
|
||||||
|
TextView officialMirrorsText = (TextView) repoView.findViewById(R.id.text_official_mirrors);
|
||||||
|
officialMirrorsText.setVisibility(View.VISIBLE);
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
for (String url : repo.mirrors) {
|
||||||
|
builder.append("◦ ");
|
||||||
|
builder.append(url);
|
||||||
|
builder.append('\n');
|
||||||
|
}
|
||||||
|
officialMirrorsText.setText(builder.toString());
|
||||||
|
}
|
||||||
|
if (repo.userMirrors != null) {
|
||||||
|
TextView userMirrorsLabel = (TextView) repoView.findViewById(R.id.label_user_mirrors);
|
||||||
|
userMirrorsLabel.setVisibility(View.VISIBLE);
|
||||||
|
TextView userMirrorsText = (TextView) repoView.findViewById(R.id.text_user_mirrors);
|
||||||
|
userMirrorsText.setVisibility(View.VISIBLE);
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
for (String url : repo.userMirrors) {
|
||||||
|
builder.append("◦ ");
|
||||||
|
builder.append(url);
|
||||||
|
builder.append('\n');
|
||||||
|
}
|
||||||
|
userMirrorsText.setText(builder.toString());
|
||||||
|
}
|
||||||
|
|
||||||
name.setText(repo.name);
|
name.setText(repo.name);
|
||||||
|
|
||||||
int appCount = RepoProvider.Helper.countAppsForRepo(this, repoId);
|
int appCount = RepoProvider.Helper.countAppsForRepo(this, repoId);
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
package sun.net.www.protocol.bluetooth;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.net.URLConnection;
|
|
||||||
import java.net.URLStreamHandler;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is added so that the bluetooth:// scheme we use for the {@link
|
|
||||||
* org.fdroid.fdroid.net.BluetoothDownloader} is not treated as invalid by
|
|
||||||
* the {@link URL} class.
|
|
||||||
*/
|
|
||||||
public class Handler extends URLStreamHandler {
|
|
||||||
@Override
|
|
||||||
protected URLConnection openConnection(URL u) throws IOException {
|
|
||||||
throw new UnsupportedOperationException("openConnection() not supported on bluetooth:// URLs");
|
|
||||||
}
|
|
||||||
}
|
|
@ -71,6 +71,30 @@
|
|||||||
android:id="@+id/text_last_update"
|
android:id="@+id/text_last_update"
|
||||||
style="@style/BodyText" />
|
style="@style/BodyText" />
|
||||||
|
|
||||||
|
<!-- mirrors included in the index -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/label_official_mirrors"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:text="@string/repo_official_mirrors"
|
||||||
|
style="@style/CaptionText" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_official_mirrors"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:textColor="@android:color/black"
|
||||||
|
style="@style/CaptionText" />
|
||||||
|
|
||||||
|
<!-- mirrors added by the user -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/label_user_mirrors"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:text="@string/repo_user_mirrors"
|
||||||
|
style="@style/CaptionText" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_user_mirrors"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:textColor="@android:color/black"
|
||||||
|
style="@style/CaptionText" />
|
||||||
|
|
||||||
<!-- The credentials used to access this repo (optional) -->
|
<!-- The credentials used to access this repo (optional) -->
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/label_username"
|
android:id="@+id/label_username"
|
||||||
|
@ -116,6 +116,7 @@ This often occurs with apps installed via Google Play or other sources, if they
|
|||||||
<string name="no">No</string>
|
<string name="no">No</string>
|
||||||
<string name="repo_add_title">Add new repository</string>
|
<string name="repo_add_title">Add new repository</string>
|
||||||
<string name="repo_add_add">Add</string>
|
<string name="repo_add_add">Add</string>
|
||||||
|
<string name="repo_add_mirror">Add mirror</string>
|
||||||
<string name="links">Links</string>
|
<string name="links">Links</string>
|
||||||
<string name="versions">Versions</string>
|
<string name="versions">Versions</string>
|
||||||
<string name="more">More</string>
|
<string name="more">More</string>
|
||||||
@ -134,12 +135,11 @@ This often occurs with apps installed via Google Play or other sources, if they
|
|||||||
|
|
||||||
<string name="repo_add_url">Repository address</string>
|
<string name="repo_add_url">Repository address</string>
|
||||||
<string name="repo_add_fingerprint">Fingerprint (optional)</string>
|
<string name="repo_add_fingerprint">Fingerprint (optional)</string>
|
||||||
<string name="repo_exists_add_fingerprint">This repo is already setup, this will add new key information.</string>
|
<string name="repo_exists_add_fingerprint">%1$s is already setup, this will add new key information.</string>
|
||||||
<string name="repo_exists_enable">This repo is already setup, confirm that you want to re-enable it.</string>
|
<string name="repo_exists_enable">%1$s is already setup, confirm that you want to re-enable it.</string>
|
||||||
<string name="repo_exists_and_enabled">The incoming repo is already setup and enabled.</string>
|
<string name="repo_exists_and_enabled">%1$s is already setup and enabled.</string>
|
||||||
<string name="repo_delete_to_overwrite">You must first delete this repo before you can add one with a different
|
<string name="repo_delete_to_overwrite">First delete %1$s in order to add this with a conflicting key.</string>
|
||||||
key.
|
<string name="repo_exists_add_mirror">This is a copy of %1$s, add it as a mirror?</string>
|
||||||
</string>
|
|
||||||
<string name="bad_fingerprint">Bad fingerprint</string>
|
<string name="bad_fingerprint">Bad fingerprint</string>
|
||||||
<string name="invalid_url">This is not a valid URL.</string>
|
<string name="invalid_url">This is not a valid URL.</string>
|
||||||
<string name="malformed_repo_uri">Ignoring malformed repo URI: %s</string>
|
<string name="malformed_repo_uri">Ignoring malformed repo URI: %s</string>
|
||||||
@ -307,6 +307,8 @@ This often occurs with apps installed via Google Play or other sources, if they
|
|||||||
<string name="repo_fingerprint">Fingerprint of the signing key (SHA-256)</string>
|
<string name="repo_fingerprint">Fingerprint of the signing key (SHA-256)</string>
|
||||||
<string name="repo_description">Description</string>
|
<string name="repo_description">Description</string>
|
||||||
<string name="repo_last_update">Last update</string>
|
<string name="repo_last_update">Last update</string>
|
||||||
|
<string name="repo_official_mirrors">Official mirrors</string>
|
||||||
|
<string name="repo_user_mirrors">User mirrors</string>
|
||||||
<string name="repo_name">Name</string>
|
<string name="repo_name">Name</string>
|
||||||
<string name="unsigned_description">This means that the list of
|
<string name="unsigned_description">This means that the list of
|
||||||
apps could not be verified. You should be careful
|
apps could not be verified. You should be careful
|
||||||
|
@ -158,22 +158,17 @@ public abstract class MultiRepoUpdaterTest extends FDroidProviderTest {
|
|||||||
return createRepo(name, uri, context, PUB_KEY);
|
return createRepo(name, uri, context, PUB_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a real instance of {@code Repo} by loading it from the database,
|
||||||
|
* that ensures it includes the primary key from the database.
|
||||||
|
*/
|
||||||
static Repo createRepo(String name, String uri, Context context, String signingCert) {
|
static Repo createRepo(String name, String uri, Context context, String signingCert) {
|
||||||
Repo repo = new Repo();
|
|
||||||
repo.signingCertificate = signingCert;
|
|
||||||
repo.address = uri;
|
|
||||||
repo.name = name;
|
|
||||||
|
|
||||||
ContentValues values = new ContentValues(3);
|
ContentValues values = new ContentValues(3);
|
||||||
values.put(Schema.RepoTable.Cols.SIGNING_CERT, repo.signingCertificate);
|
values.put(Schema.RepoTable.Cols.SIGNING_CERT, signingCert);
|
||||||
values.put(Schema.RepoTable.Cols.ADDRESS, repo.address);
|
values.put(Schema.RepoTable.Cols.ADDRESS, uri);
|
||||||
values.put(Schema.RepoTable.Cols.NAME, repo.name);
|
values.put(Schema.RepoTable.Cols.NAME, name);
|
||||||
|
|
||||||
RepoProvider.Helper.insert(context, values);
|
RepoProvider.Helper.insert(context, values);
|
||||||
|
return RepoProvider.Helper.findByAddress(context, uri);
|
||||||
// Need to reload the repo based on address so that it includes the primary key from
|
|
||||||
// the database.
|
|
||||||
return RepoProvider.Helper.findByAddress(context, repo.address);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected RepoUpdater createRepoUpdater(String name, String uri, Context context) {
|
protected RepoUpdater createRepoUpdater(String name, String uri, Context context) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user