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:
Hans-Christoph Steiner 2018-03-29 21:59:26 +00:00
commit 5f2b053b1c
24 changed files with 442 additions and 293 deletions

View File

@ -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 {

View File

@ -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

View File

@ -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!");
} }
} }

View File

@ -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);

View File

@ -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);
} }

View File

@ -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!");
} }
/** /**

View File

@ -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);

View File

@ -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();

View File

@ -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();

View File

@ -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) {

View File

@ -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);
} }
} }

View File

@ -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,
}; };
} }
} }

View File

@ -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;
} }

View File

@ -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);
} }
} }
}; };

View File

@ -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;

View File

@ -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) {

View File

@ -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

View File

@ -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;
} }

View File

@ -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();

View File

@ -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);

View File

@ -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");
}
}

View File

@ -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"

View File

@ -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

View File

@ -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) {