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 {
compileSdkVersion 24
buildToolsVersion '25.0.2'
buildToolsVersion '25.0.3'
useLibrary 'org.apache.http.legacy'
buildTypes {

View File

@ -60,12 +60,8 @@ import org.fdroid.fdroid.installer.InstallHistoryService;
import org.fdroid.fdroid.net.ImageLoaderForUIL;
import org.fdroid.fdroid.net.WifiStateChangeService;
import org.fdroid.fdroid.views.hiding.HidingManager;
import sun.net.www.protocol.bluetooth.Handler;
import java.io.IOException;
import java.net.URL;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.security.Security;
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;
Preferences.get().registerUnstableUpdatesChangeListener(new Preferences.ChangeListener() {
@Override

View File

@ -6,7 +6,6 @@ import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonFactory;
@ -15,7 +14,6 @@ import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.InjectableValues;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App;
@ -31,7 +29,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Date;
@ -135,7 +132,7 @@ public class IndexV1Updater extends RepoUpdater {
if (downloader != null) {
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) {
// ignored if canceled, the local database just won't be updated
}
@ -144,7 +141,7 @@ public class IndexV1Updater extends RepoUpdater {
if (downloader != null) {
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) {
// 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);
JarEntry indexEntry = (JarEntry) jarFile.getEntry(DATA_FILE_NAME);
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);
}
@ -236,7 +233,7 @@ public class IndexV1Updater extends RepoUpdater {
long timestamp = (Long) repoMap.get("timestamp") / 1000;
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);
}
@ -410,16 +407,14 @@ public class IndexV1Updater extends RepoUpdater {
String certFromJar = Hasher.hex(rawCertFromJar);
if (TextUtils.isEmpty(certFromJar)) {
throw new SigningException(repo,
SIGNED_FILE_NAME + " must have an included signing certificate!");
throw new SigningException(SIGNED_FILE_NAME + " must have an included signing certificate!");
}
if (repo.signingCertificate == null) {
if (repo.fingerprint != null) {
String fingerprintFromJar = Utils.calcFingerprint(rawCertFromJar);
if (!repo.fingerprint.equalsIgnoreCase(fingerprintFromJar)) {
throw new SigningException(repo,
"Supplied certificate fingerprint does not match!");
throw new SigningException("Supplied certificate fingerprint does not match!");
}
}
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)) {
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)) {
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.IOException;
import java.io.InputStream;
import java.net.URL;
class ProgressBufferedInputStream extends BufferedInputStream {
private final ProgressListener progressListener;
private final URL sourceUrl;
private final String urlString;
private final int totalBytes;
private int currentBytes;
@ -17,10 +16,10 @@ class ProgressBufferedInputStream extends BufferedInputStream {
* Reports progress to the specified {@link ProgressListener}, with the
* 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);
this.progressListener = progressListener;
this.sourceUrl = sourceUrl;
this.urlString = urlString;
this.totalBytes = totalBytes;
}
@ -32,7 +31,7 @@ class ProgressBufferedInputStream extends BufferedInputStream {
* the digits changing because it looks pretty, < 9000 since the reads won't
* line up exactly */
if (currentBytes % 333333 < 9000) {
progressListener.onProgress(sourceUrl, currentBytes, totalBytes);
progressListener.onProgress(urlString, currentBytes, totalBytes);
}
}
return super.read(buffer, byteOffset, byteCount);

View File

@ -19,6 +19,6 @@ import java.net.URL;
*/
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.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.CodeSigner;
import java.security.cert.Certificate;
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) {
// ignored if canceled, the local database just won't be updated
e.printStackTrace();
@ -202,7 +201,7 @@ public class RepoUpdater {
InputStream indexInputStream = null;
try {
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
@ -213,7 +212,7 @@ public class RepoUpdater {
JarFile jarFile = new JarFile(downloadedFile, true);
JarEntry indexEntry = (JarEntry) jarFile.getEntry("index.xml");
indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry),
processIndexListener, new URL(repo.address), (int) indexEntry.getSize());
processIndexListener, repo.address, (int) indexEntry.getSize());
// Process the index...
SAXParserFactory factory = SAXParserFactory.newInstance();
@ -226,7 +225,7 @@ public class RepoUpdater {
long timestamp = repoDetailsToSave.getAsLong(RepoTable.Cols.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);
}
@ -237,7 +236,7 @@ public class RepoUpdater {
assertSigningCertFromXmlCorrect();
commitToDb();
} catch (SAXException | ParserConfigurationException | IOException e) {
throw new UpdateException(repo, "Error parsing index", e);
throw new UpdateException("Error parsing index", e);
} finally {
FDroidApp.enableSpongyCastleOnLollipop();
Utils.closeQuietly(indexInputStream);
@ -251,14 +250,14 @@ public class RepoUpdater {
protected final ProgressListener downloadListener = new ProgressListener() {
@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);
}
};
protected final ProgressListener processIndexListener = new ProgressListener() {
@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);
}
};
@ -343,22 +342,19 @@ public class RepoUpdater {
public static class UpdateException extends Exception {
private static final long serialVersionUID = -4492452418826132803L;
public final Repo repo;
public UpdateException(Repo repo, String message) {
public UpdateException(String message) {
super(message);
this.repo = repo;
}
public UpdateException(Repo repo, String message, Exception cause) {
public UpdateException(String message, Exception cause) {
super(message, cause);
this.repo = repo;
}
}
public static class SigningException extends UpdateException {
public SigningException(Repo repo, String message) {
super(repo, "Repository was not signed correctly: " + message);
public SigningException(String 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
* restrictions.
*/
X509Certificate getSigningCertFromJar(JarEntry jarEntry) throws SigningException {
public static X509Certificate getSigningCertFromJar(JarEntry jarEntry) throws SigningException {
final CodeSigner[] codeSigners = jarEntry.getCodeSigners();
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 */
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();
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);
}
@ -404,7 +400,7 @@ public class RepoUpdater {
String fingerprintFromJar = Utils.calcFingerprint(rawCertFromJar);
if (!repo.fingerprint.equalsIgnoreCase(fingerprintFromIndexXml)
|| !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
@ -435,7 +431,7 @@ public class RepoUpdater {
if (TextUtils.isEmpty(repo.signingCertificate)
|| TextUtils.isEmpty(certFromJar)
|| 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
@ -444,7 +440,7 @@ public class RepoUpdater {
&& certFromIndexXml.equals(certFromJar)) {
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 + ")");
String downloadedSizeFriendly = Utils.getFriendlySize(bytesRead);
int percent = -1;
if (totalBytes > 0) {
percent = (int) ((double) bytesRead / totalBytes * 100);
percent = (int) (bytesRead / (totalBytes * 100L));
}
String message;
if (totalBytes == -1) {
@ -534,13 +535,14 @@ public class UpdateService extends IntentService {
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 + ")");
String downloadedSize = Utils.getFriendlySize(bytesRead);
String totalSize = Utils.getFriendlySize(totalBytes);
int percent = -1;
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);
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.ICON + " string, "
+ RepoTable.Cols.MIRRORS + " string, "
+ RepoTable.Cols.USER_MIRRORS + " string, "
+ 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 + ") "
+ " );";
protected static final int DB_VERSION = 77;
protected static final int DB_VERSION = 78;
private final Context context;
@ -321,6 +322,17 @@ public class DBHelper extends SQLiteOpenHelper {
addApkAntiFeatures(db, oldVersion);
addIgnoreVulnPref(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) {

View File

@ -4,7 +4,6 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.localrepo.peers.WifiPeer;
@ -82,17 +81,12 @@ public class NewRepoConfig {
scheme = scheme.toLowerCase(Locale.ENGLISH);
host = host.toLowerCase(Locale.ENGLISH);
// We only listen for /fdroid/archive or /fdroid/repo paths when receiving a HTTP(S) intent.
// For fdroidrepo(s) intents, we are less picky and will accept any path.
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"))) {
if (uri.getPath() == null
|| !Arrays.asList("https", "http", "fdroidrepos", "fdroidrepo").contains(scheme)) {
isValidRepo = false;
return;
}
boolean isFdroidScheme = TextUtils.equals("fdroidrepo", scheme) || TextUtils.equals("fdroidrepos", scheme);
String userInfo = uri.getUserInfo();
if (userInfo != null) {
String[] userInfoTokens = userInfo.split(":");
@ -109,15 +103,8 @@ public class NewRepoConfig {
bssid = uri.getQueryParameter("bssid");
ssid = uri.getQueryParameter("ssid");
fromSwap = uri.getQueryParameter("swap") != null;
if (!isFdroidScheme && !isHttpScheme) {
isValidRepo = false;
return;
}
uriString = sanitizeRepoUri(uri);
isValidRepo = true;
}
public String getBssid() {
@ -175,7 +162,9 @@ public class NewRepoConfig {
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) {
String scheme = uri.getScheme();
String host = uri.getHost();

View File

@ -26,13 +26,13 @@ package org.fdroid.fdroid.data;
import android.content.ContentValues;
import android.database.Cursor;
import android.text.TextUtils;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Schema.RepoTable.Cols;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
@ -94,6 +94,11 @@ public class Repo extends ValueObject {
/** Official mirrors of this repo, considered automatically interchangeable */
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 */
public int pushRequests = PUSH_REQUEST_IGNORE;
@ -160,6 +165,9 @@ public class Repo extends ValueObject {
case Cols.MIRRORS:
mirrors = Utils.parseCommaSeparatedString(cursor.getString(i));
break;
case Cols.USER_MIRRORS:
userMirrors = Utils.parseCommaSeparatedString(cursor.getString(i));
break;
case Cols.PUSH_REQUESTS:
pushRequests = cursor.getInt(i);
break;
@ -297,19 +305,37 @@ public class Repo extends ValueObject {
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)) {
pushRequests = toInt(values.getAsInteger(Cols.PUSH_REQUESTS));
}
}
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() {
int count = 0;
if (mirrors != null && mirrors.length > 1) {
for (String m: mirrors) {
for (String m : getMirrorList()) {
if (!m.equals(address)) {
if (FDroidApp.isUsingTor()) {
count++;
@ -320,7 +346,6 @@ public class Repo extends ValueObject {
}
}
}
}
return count;
}
@ -328,7 +353,7 @@ public class Repo extends ValueObject {
if (TextUtils.isEmpty(lastWorkingMirror)) {
lastWorkingMirror = address;
}
List<String> shuffledMirrors = Arrays.asList(mirrors);
List<String> shuffledMirrors = getMirrorList();
Collections.shuffle(shuffledMirrors);
if (shuffledMirrors.size() > 1) {
for (String m : shuffledMirrors) {

View File

@ -106,7 +106,7 @@ public class RepoPersister {
try {
context.getContentResolver().applyBatch(TempApkProvider.getAuthority(), apkOperations);
} 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);
return getIdsForPackages(appsToSave);
} 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 ICON = "icon";
String MIRRORS = "mirrors";
String USER_MIRRORS = "userMirrors";
String PUSH_REQUESTS = "pushRequests";
String[] ALL = {
_ID, ADDRESS, NAME, DESCRIPTION, IN_USE, PRIORITY, SIGNING_CERT,
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;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.util.Log;
import org.apache.commons.io.input.BoundedInputStream;
import org.fdroid.fdroid.Utils;
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.IOException;
import java.io.InputStream;
import java.net.URL;
public class BluetoothDownloader extends Downloader {
@ -24,10 +23,11 @@ public class BluetoothDownloader extends Downloader {
private FileDetails fileDetails;
private final String sourcePath;
public BluetoothDownloader(String macAddress, URL sourceUrl, File destFile) throws IOException {
super(sourceUrl, destFile);
public BluetoothDownloader(Uri uri, File destFile) throws IOException {
super(uri, destFile);
String macAddress = uri.getHost().replace("-", ":");
this.connection = new BluetoothClient(macAddress).openConnection();
this.sourcePath = sourceUrl.getPath();
this.sourcePath = uri.getPath();
}
@Override
@ -58,7 +58,7 @@ public class BluetoothDownloader extends Downloader {
if (fileDetails == null) {
Utils.debugLog(TAG, "Going to Bluetooth \"server\" to get file details.");
try {
fileDetails = Request.createHEAD(sourceUrl.getPath(), connection).send().toFileDetails();
fileDetails = Request.createHEAD(sourcePath, connection).send().toFileDetails();
} catch (IOException e) {
Log.e(TAG, "Error getting file details from Bluetooth \"server\"", e);
}
@ -73,7 +73,7 @@ public class BluetoothDownloader extends Downloader {
}
@Override
public int totalDownloadSize() {
public long totalDownloadSize() {
FileDetails details = getFileDetails();
return details != null ? details.getFileSize() : -1;
}

View File

@ -1,5 +1,6 @@
package org.fdroid.fdroid.net;
import android.net.Uri;
import org.fdroid.fdroid.ProgressListener;
import org.fdroid.fdroid.Utils;
@ -9,7 +10,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.URL;
import java.util.Timer;
import java.util.TimerTask;
@ -32,12 +32,12 @@ public abstract class Downloader {
public static final String EXTRA_MIRROR_URL = "org.fdroid.fdroid.net.Downloader.extra.ERROR_MIRROR_URL";
private volatile boolean cancelled = false;
private volatile int bytesRead;
private volatile int totalBytes;
private volatile long bytesRead;
private volatile long totalBytes;
public final File outputFile;
final URL sourceUrl;
final String urlString;
String cacheTag;
boolean notFound;
@ -52,8 +52,8 @@ public abstract class Downloader {
protected abstract void close();
Downloader(URL url, File destFile) {
this.sourceUrl = url;
Downloader(Uri uri, File destFile) {
this.urlString = uri.toString();
outputFile = destFile;
}
@ -92,7 +92,7 @@ public abstract class Downloader {
public abstract boolean hasChanged();
protected abstract int totalDownloadSize();
protected abstract long totalDownloadSize();
public abstract void download() throws ConnectException, IOException, InterruptedException;
@ -201,7 +201,7 @@ public abstract class Downloader {
@Override
public void run() {
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.net.Uri;
import android.support.v4.content.LocalBroadcastManager;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.data.Schema;
import java.io.File;
import java.io.IOException;
import java.net.URL;
public class DownloaderFactory {
private static LocalBroadcastManager localBroadcastManager;
/**
* Downloads to a temporary file, which *you must delete yourself when
* 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)
throws IOException {
URL url = new URL(urlString);
Downloader downloader;
if (localBroadcastManager == null) {
localBroadcastManager = LocalBroadcastManager.getInstance(context);
}
if ("bluetooth".equalsIgnoreCase(url.getProtocol())) {
String macAddress = url.getHost().replace("-", ":");
downloader = new BluetoothDownloader(macAddress, url, destFile);
Uri uri = Uri.parse(urlString);
String scheme = uri.getScheme();
if ("bluetooth".equals(scheme)) {
downloader = new BluetoothDownloader(uri, destFile);
} else {
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) {
downloader = new HttpDownloader(url, destFile);
downloader = new HttpDownloader(uri, destFile);
} else {
downloader = new HttpDownloader(url, destFile, repo.username, repo.password);
downloader = new HttpDownloader(uri, destFile, repo.username, repo.password);
}
}
return downloader;

View File

@ -31,7 +31,6 @@ import android.os.PatternMatcher;
import android.os.Process;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import org.fdroid.fdroid.ProgressListener;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
@ -42,7 +41,6 @@ import java.io.File;
import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.URL;
/**
* DownloaderService is a service that handles asynchronous download requests
@ -199,7 +197,7 @@ public class DownloaderService extends Service {
downloader = DownloaderFactory.create(this, uri, localFile);
downloader.setListener(new ProgressListener() {
@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.setData(uri);
intent.putExtra(Downloader.EXTRA_BYTES_READ, bytesRead);
@ -321,7 +319,7 @@ public class DownloaderService extends Service {
* Check if a URL is actively being downloaded.
*/
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) {

View File

@ -1,5 +1,8 @@
package org.fdroid.fdroid.net;
import android.annotation.TargetApi;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
import info.guardianproject.netcipher.NetCipher;
@ -27,29 +30,30 @@ public class HttpDownloader extends Downloader {
private final String username;
private final String password;
private URL sourceUrl;
private HttpURLConnection connection;
private boolean newFileAvailableOnServer;
HttpDownloader(URL url, File destFile)
HttpDownloader(Uri uri, File destFile)
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
* {@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 username Username for HTTP Basic Auth, use {@code null} to ignore
* @param password Password for HTTP Basic Auth, use {@code null} to ignore
* @throws FileNotFoundException
* @throws MalformedURLException
*/
HttpDownloader(URL url, File destFile, String username, String password)
HttpDownloader(Uri uri, File destFile, String username, String password)
throws FileNotFoundException, MalformedURLException {
super(url, destFile);
super(uri, destFile);
this.sourceUrl = new URL(urlString);
this.username = username;
this.password = password;
}
@ -93,7 +97,7 @@ public class HttpDownloader extends Downloader {
case 200:
contentLength = tmpConn.getContentLength();
if (!TextUtils.isEmpty(etag) && etag.equals(cacheTag)) {
Utils.debugLog(TAG, sourceUrl + " is cached, not downloading");
Utils.debugLog(TAG, urlString + " is cached, not downloading");
return;
}
newFileAvailableOnServer = true;
@ -102,7 +106,7 @@ public class HttpDownloader extends Downloader {
notFound = true;
return;
default:
Utils.debugLog(TAG, "HEAD check of " + sourceUrl + " returned " + statusCode + ": "
Utils.debugLog(TAG, "HEAD check of " + urlString + " returned " + statusCode + ": "
+ tmpConn.getResponseMessage());
}
@ -116,7 +120,7 @@ public class HttpDownloader extends Downloader {
resumable = true;
}
setupConnection(resumable);
Utils.debugLog(TAG, "downloading " + sourceUrl + " (is resumable: " + resumable + ")");
Utils.debugLog(TAG, "downloading " + urlString + " (is resumable: " + resumable + ")");
downloadFromStream(8192, resumable);
cacheTag = connection.getHeaderField(HEADER_FIELD_ETAG);
}
@ -169,8 +173,13 @@ public class HttpDownloader extends Downloader {
// because as the repo grows, the tradeoff will
// become more worth it.
@Override
public int totalDownloadSize() {
@TargetApi(24)
public long totalDownloadSize() {
if (Build.VERSION.SDK_INT < 24) {
return connection.getContentLength();
} else {
return connection.getContentLengthLong();
}
}
@Override

View File

@ -3,13 +3,13 @@ package org.fdroid.fdroid.net.bluetooth;
public class FileDetails {
private String cacheTag;
private int fileSize;
private long fileSize;
public String getCacheTag() {
return cacheTag;
}
public int getFileSize() {
public long getFileSize() {
return fileSize;
}

View File

@ -19,6 +19,7 @@
package org.fdroid.fdroid.views;
import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.Context;
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.Schema.RepoTable;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
@SuppressWarnings("LineLength")
public class ManageReposActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor>, RepoAdapter.EnabledListener {
public class ManageReposActivity extends AppCompatActivity
implements LoaderManager.LoaderCallbacks<Cursor>, RepoAdapter.EnabledListener {
private static final String TAG = "ManageReposActivity";
private static final String DEFAULT_NEW_REPO_TEXT = "https://";
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,
IS_SWAP
}
@ -213,18 +217,35 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
private class AddRepo {
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 TextView overwriteMessage;
private final ColorStateList defaultTextColour;
private final Button addButton;
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) {
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);
addRepoDialog = new AlertDialog.Builder(context).setView(view).create();
final EditText uriEditText = (EditText) view.findViewById(R.id.edit_uri);
@ -275,7 +296,7 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
try {
url = normalizeUrl(url);
} catch (URISyntaxException e) {
invalidUrl();
invalidUrl(null);
return;
}
@ -289,7 +310,8 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
break;
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);
RepoProvider.Helper.remove(context, repo.getId());
prepareToCreateNewRepo(url, fp, username, password);
@ -297,7 +319,7 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
case EXISTS_DISABLED:
case EXISTS_UPGRADABLE_TO_SIGNED:
case EXISTS_FINGERPRINT_MATCH:
case EXISTS_ADD_MIRROR:
updateAndEnableExistingRepo(url, fp);
finishedAddingRepo();
break;
@ -347,9 +369,31 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
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
* 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) {
@ -361,72 +405,84 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
// 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) {
repoDoesntExist();
repoDoesntExist(repo);
} else {
if (repo.isSwap) {
repoIsSwap();
repoIsSwap(repo);
} else if (repo.fingerprint == null && fingerprint.length() > 0) {
upgradingToSigned();
upgradingToSigned(repo);
} else if (repo.fingerprint != null && !repo.fingerprint.equalsIgnoreCase(fingerprint)) {
repoFingerprintDoesntMatch();
repoFingerprintDoesntMatch(repo);
} else {
// Could be either an unsigned repo, and no fingerprint was supplied,
// or it could be a signed repo with a matching fingerprint.
if (repo.inuse) {
repoExistsAndEnabled();
if (!TextUtils.equals(repo.address, uri)
&& !repo.getMirrorList().contains(uri)) {
repoExistsAddMirror(repo);
} else if (repo.inuse) {
repoExistsAndEnabled(repo);
} else {
repoExistsAndDisabled();
repoExistsAndDisabled(repo);
}
}
}
}
private void repoDoesntExist() {
updateUi(AddRepoState.DOESNT_EXIST, 0, false, R.string.repo_add_add, true);
private void repoDoesntExist(Repo repo) {
updateUi(repo, AddRepoState.DOESNT_EXIST, 0, false, R.string.repo_add_add, true);
}
private void repoIsSwap() {
updateUi(AddRepoState.IS_SWAP, 0, false, R.string.repo_add_add, true);
private void repoIsSwap(Repo repo) {
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
* force the user to manually delete the repo before adding this one.
*/
private void repoFingerprintDoesntMatch() {
updateUi(AddRepoState.EXISTS_FINGERPRINT_MISMATCH, R.string.repo_delete_to_overwrite,
private void repoFingerprintDoesntMatch(Repo repo) {
updateUi(repo, AddRepoState.EXISTS_FINGERPRINT_MISMATCH,
R.string.repo_delete_to_overwrite,
true, R.string.overwrite, false);
}
private void invalidUrl() {
updateUi(AddRepoState.INVALID_URL, R.string.invalid_url, true,
private void invalidUrl(Repo repo) {
updateUi(repo, AddRepoState.INVALID_URL, R.string.invalid_url, true,
R.string.repo_add_add, false);
}
private void repoExistsAndDisabled() {
updateUi(AddRepoState.EXISTS_DISABLED,
private void repoExistsAndDisabled(Repo repo) {
updateUi(repo, AddRepoState.EXISTS_DISABLED,
R.string.repo_exists_enable, false, R.string.enable, true);
}
private void repoExistsAndEnabled() {
updateUi(AddRepoState.EXISTS_ENABLED, R.string.repo_exists_and_enabled, false,
private void repoExistsAndEnabled(Repo repo) {
updateUi(repo, AddRepoState.EXISTS_ENABLED, R.string.repo_exists_and_enabled, false,
R.string.ok, true);
}
private void upgradingToSigned() {
updateUi(AddRepoState.EXISTS_UPGRADABLE_TO_SIGNED, R.string.repo_exists_add_fingerprint,
private void repoExistsAddMirror(Repo repo) {
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);
}
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) {
addRepoState = state;
if (messageRes > 0) {
overwriteMessage.setText(messageRes);
overwriteMessage.setText(String.format(getString(messageRes), repo.name));
overwriteMessage.setVisibility(View.VISIBLE);
if (redMessage) {
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.
*/
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);
addRepoDialog.getButton(AlertDialog.BUTTON_POSITIVE).setVisibility(View.GONE);
final View addRepoForm = addRepoDialog.findViewById(R.id.add_repo_form);
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);
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>() {
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
protected String doInBackground(String... params) {
final String originalAddress = params[0];
if (fingerprintRepoMap.containsKey(fingerprint)) {
statusCode = REFRESH_DIALOG;
return originalAddress;
}
final String[] pathsToCheck = {"", "fdroid/repo", "repo"};
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);
final String addressWithoutIndex = builder.build().toString();
publishProgress(addressWithoutIndex);
if (urlRepoMap.containsKey(addressWithoutIndex)) {
statusCode = REFRESH_DIALOG;
return addressWithoutIndex;
}
final Uri uri = builder.appendPath("index.jar").build();
try {
@ -482,7 +558,7 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
}
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;
}
}
@ -491,14 +567,13 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
}
private boolean checkForRepository(Uri indexUri) throws IOException {
final URL url = new URL(indexUri.toString());
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("HEAD");
statusCode = connection.getResponseCode();
return statusCode == 401 || statusCode == 200;
return statusCode == HTTP_UNAUTHORIZED || statusCode == HTTP_OK;
}
@Override
@ -512,10 +587,11 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
if (addRepoDialog.isShowing()) {
if (statusCode == 401) {
if (statusCode == HTTP_UNAUTHORIZED) {
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 passwordInput = (EditText) view.findViewById(R.id.edit_password);
@ -543,12 +619,21 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
new DialogInterface.OnClickListener() {
@Override
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();
} 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 {
// 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() {
@Override
public void onClick(View v) {
@ -615,7 +698,8 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
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 {
address = normalizeUrl(address);
} catch (URISyntaxException e) {
@ -634,7 +718,7 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
RepoProvider.Helper.insert(context, values);
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);
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);
values.put(RepoTable.Cols.IN_USE, 1);
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);
notifyDataSetChanged();
finishedAddingRepo();
}
@ -675,7 +782,6 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
finish();
}
}
}
private void addRepoFromIntent(Intent intent) {
@ -683,7 +789,8 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana
NewRepoConfig newRepoConfig = new NewRepoConfig(this, intent);
if (newRepoConfig.isValidRepo()) {
isImportingRepo = true;
showAddRepo(newRepoConfig.getRepoUriString(), newRepoConfig.getFingerprint(), newRepoConfig.getUsername(), newRepoConfig.getPassword());
showAddRepo(newRepoConfig.getRepoUriString(), newRepoConfig.getFingerprint(),
newRepoConfig.getUsername(), newRepoConfig.getPassword());
checkIfNewRepoOnSameWifi(newRepoConfig);
} else if (newRepoConfig.getErrorMessage() != null) {
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.ADDRESS,
RepoTable.Cols.FINGERPRINT,
RepoTable.Cols.MIRRORS,
RepoTable.Cols.USER_MIRRORS,
};
repo = RepoProvider.Helper.findById(this, repoId, projection);
TextView inputUrl = (TextView) findViewById(R.id.input_repo_url);
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.buildUpon().appendQueryParameter("fingerprint", repo.fingerprint).build();
String qrUriString = uri.toString();
@ -321,6 +328,33 @@ public class RepoDetailsActivity extends ActionBarActivity {
TextView numApps = (TextView) repoView.findViewById(R.id.text_num_apps);
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);
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"
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) -->
<TextView
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="repo_add_title">Add new repository</string>
<string name="repo_add_add">Add</string>
<string name="repo_add_mirror">Add mirror</string>
<string name="links">Links</string>
<string name="versions">Versions</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_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_enable">This repo 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_delete_to_overwrite">You must first delete this repo before you can add one with a different
key.
</string>
<string name="repo_exists_add_fingerprint">%1$s is already setup, this will add new key information.</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">%1$s is already setup and enabled.</string>
<string name="repo_delete_to_overwrite">First delete %1$s in order to add this with a conflicting key.</string>
<string name="repo_exists_add_mirror">This is a copy of %1$s, add it as a mirror?</string>
<string name="bad_fingerprint">Bad fingerprint</string>
<string name="invalid_url">This is not a valid URL.</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_description">Description</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="unsigned_description">This means that the list of
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);
}
/**
* 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) {
Repo repo = new Repo();
repo.signingCertificate = signingCert;
repo.address = uri;
repo.name = name;
ContentValues values = new ContentValues(3);
values.put(Schema.RepoTable.Cols.SIGNING_CERT, repo.signingCertificate);
values.put(Schema.RepoTable.Cols.ADDRESS, repo.address);
values.put(Schema.RepoTable.Cols.NAME, repo.name);
values.put(Schema.RepoTable.Cols.SIGNING_CERT, signingCert);
values.put(Schema.RepoTable.Cols.ADDRESS, uri);
values.put(Schema.RepoTable.Cols.NAME, name);
RepoProvider.Helper.insert(context, values);
// 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);
return RepoProvider.Helper.findByAddress(context, uri);
}
protected RepoUpdater createRepoUpdater(String name, String uri, Context context) {