diff --git a/app/build.gradle b/app/build.gradle index 85bb92ffd..7611374d2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 { diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index 58b16de3f..0d3a05068 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -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 diff --git a/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java b/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java index fd0f1563b..b961f3abb 100644 --- a/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java +++ b/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java @@ -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!"); } } diff --git a/app/src/main/java/org/fdroid/fdroid/ProgressBufferedInputStream.java b/app/src/main/java/org/fdroid/fdroid/ProgressBufferedInputStream.java index d8ca7ca9f..e849fe3cb 100644 --- a/app/src/main/java/org/fdroid/fdroid/ProgressBufferedInputStream.java +++ b/app/src/main/java/org/fdroid/fdroid/ProgressBufferedInputStream.java @@ -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); diff --git a/app/src/main/java/org/fdroid/fdroid/ProgressListener.java b/app/src/main/java/org/fdroid/fdroid/ProgressListener.java index 1e71373ad..dc9fbcbdb 100644 --- a/app/src/main/java/org/fdroid/fdroid/ProgressListener.java +++ b/app/src/main/java/org/fdroid/fdroid/ProgressListener.java @@ -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); } diff --git a/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java b/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java index 2ac6e7da9..6e8654d89 100644 --- a/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java +++ b/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java @@ -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 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!"); } /** diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index 1b9a5a62b..5b233046f 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -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); diff --git a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java index 45a936473..e0534384f 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java @@ -88,35 +88,36 @@ 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 + ");"; static final String CREATE_TABLE_APK = "CREATE TABLE " + ApkTable.NAME + " ( " - + ApkTable.Cols.APP_ID + " integer not null, " - + ApkTable.Cols.VERSION_NAME + " text not null, " - + ApkTable.Cols.REPO_ID + " integer not null, " - + ApkTable.Cols.HASH + " text not null, " - + ApkTable.Cols.VERSION_CODE + " int not null," - + ApkTable.Cols.NAME + " text not null, " - + ApkTable.Cols.SIZE + " int not null, " - + ApkTable.Cols.SIGNATURE + " string, " - + ApkTable.Cols.SOURCE_NAME + " string, " - + ApkTable.Cols.MIN_SDK_VERSION + " integer, " - + ApkTable.Cols.TARGET_SDK_VERSION + " integer, " - + ApkTable.Cols.MAX_SDK_VERSION + " integer, " - + ApkTable.Cols.OBB_MAIN_FILE + " string, " - + ApkTable.Cols.OBB_MAIN_FILE_SHA256 + " string, " - + ApkTable.Cols.OBB_PATCH_FILE + " string, " - + ApkTable.Cols.OBB_PATCH_FILE_SHA256 + " string, " - + ApkTable.Cols.REQUESTED_PERMISSIONS + " string, " - + ApkTable.Cols.FEATURES + " string, " - + ApkTable.Cols.NATIVE_CODE + " string, " - + ApkTable.Cols.HASH_TYPE + " string, " - + ApkTable.Cols.ADDED_DATE + " string, " - + ApkTable.Cols.IS_COMPATIBLE + " int not null, " - + ApkTable.Cols.INCOMPATIBLE_REASONS + " text" - + ");"; + + ApkTable.Cols.APP_ID + " integer not null, " + + ApkTable.Cols.VERSION_NAME + " text not null, " + + ApkTable.Cols.REPO_ID + " integer not null, " + + ApkTable.Cols.HASH + " text not null, " + + ApkTable.Cols.VERSION_CODE + " int not null," + + ApkTable.Cols.NAME + " text not null, " + + ApkTable.Cols.SIZE + " int not null, " + + ApkTable.Cols.SIGNATURE + " string, " + + ApkTable.Cols.SOURCE_NAME + " string, " + + ApkTable.Cols.MIN_SDK_VERSION + " integer, " + + ApkTable.Cols.TARGET_SDK_VERSION + " integer, " + + ApkTable.Cols.MAX_SDK_VERSION + " integer, " + + ApkTable.Cols.OBB_MAIN_FILE + " string, " + + ApkTable.Cols.OBB_MAIN_FILE_SHA256 + " string, " + + ApkTable.Cols.OBB_PATCH_FILE + " string, " + + ApkTable.Cols.OBB_PATCH_FILE_SHA256 + " string, " + + ApkTable.Cols.REQUESTED_PERMISSIONS + " string, " + + ApkTable.Cols.FEATURES + " string, " + + ApkTable.Cols.NATIVE_CODE + " string, " + + ApkTable.Cols.HASH_TYPE + " string, " + + ApkTable.Cols.ADDED_DATE + " string, " + + ApkTable.Cols.IS_COMPATIBLE + " int not null, " + + ApkTable.Cols.INCOMPATIBLE_REASONS + " text" + + ");"; static final String CREATE_TABLE_APP_METADATA = "CREATE TABLE " + AppMetadataTable.NAME + " ( " @@ -181,7 +182,7 @@ public class DBHelper extends SQLiteOpenHelper { * app metadata id, because it can instead look through the primary key index. This can be * observed by flipping the order of the primary key columns, and noting the resulting sqlite * logs along the lines of: - * E/SQLiteLog(14164): (284) automatic index on fdroid_categoryAppMetadataJoin(appMetadataId) + * E/SQLiteLog(14164): (284) automatic index on fdroid_categoryAppMetadataJoin(appMetadataId) */ static final String CREATE_TABLE_CAT_JOIN = "CREATE TABLE " + CatJoinTable.NAME + " ( " @@ -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) { @@ -581,7 +593,7 @@ public class DBHelper extends SQLiteOpenHelper { updateRepoPriority(db, gpPubKey, gpArchiveAddress, 4); 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. // 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, values, RepoTable.Cols.SIGNING_CERT + " = ? AND " + RepoTable.Cols.ADDRESS + " = ?", - new String[] {signingCert, address} + new String[]{signingCert, address} ); } @@ -630,15 +642,15 @@ public class DBHelper extends SQLiteOpenHelper { Utils.debugLog(TAG, "Migrating app preferences to separate table"); db.execSQL( "INSERT INTO " + AppPrefsTable.NAME + " (" - + AppPrefsTable.Cols.PACKAGE_NAME + ", " - + AppPrefsTable.Cols.IGNORE_THIS_UPDATE + ", " - + AppPrefsTable.Cols.IGNORE_ALL_UPDATES - + ") SELECT " - + "id, " - + "ignoreThisUpdate, " - + "ignoreAllUpdates " - + "FROM " + AppMetadataTable.NAME + " " - + "WHERE ignoreThisUpdate > 0 OR ignoreAllUpdates > 0" + + AppPrefsTable.Cols.PACKAGE_NAME + ", " + + AppPrefsTable.Cols.IGNORE_THIS_UPDATE + ", " + + AppPrefsTable.Cols.IGNORE_ALL_UPDATES + + ") SELECT " + + "id, " + + "ignoreThisUpdate, " + + "ignoreAllUpdates " + + "FROM " + AppMetadataTable.NAME + " " + + "WHERE ignoreThisUpdate > 0 OR ignoreAllUpdates > 0" ); resetTransient(db); @@ -687,7 +699,7 @@ public class DBHelper extends SQLiteOpenHelper { db.execSQL(createTableDdl); - String nonPackageNameFields = TextUtils.join(", ", new String[] { + String nonPackageNameFields = TextUtils.join(", ", new String[]{ ApkTable.Cols.APP_ID, ApkTable.Cols.VERSION_NAME, ApkTable.Cols.REPO_ID, @@ -766,7 +778,7 @@ public class DBHelper extends SQLiteOpenHelper { } List oldrepos = new ArrayList<>(); 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); if (cursor != null) { if (cursor.getCount() > 0) { @@ -847,7 +859,7 @@ public class DBHelper extends SQLiteOpenHelper { } List oldrepos = new ArrayList<>(); 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); if (cursor != null) { if (cursor.getCount() > 0) { @@ -865,7 +877,7 @@ public class DBHelper extends SQLiteOpenHelper { for (final Repo repo : oldrepos) { ContentValues values = new ContentValues(); 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); - String nonIdFields = TextUtils.join(", ", new String[] { + String nonIdFields = TextUtils.join(", ", new String[]{ RepoTable.Cols.ADDRESS, RepoTable.Cols.NAME, RepoTable.Cols.DESCRIPTION, @@ -1235,8 +1247,8 @@ public class DBHelper extends SQLiteOpenHelper { } private static boolean tableExists(SQLiteDatabase db, String table) { - Cursor cursor = db.query("sqlite_master", new String[] {"name"}, - "type = 'table' AND name = ?", new String[] {table}, null, null, null); + Cursor cursor = db.query("sqlite_master", new String[]{"name"}, + "type = 'table' AND name = ?", new String[]{table}, null, null, null); boolean exists = cursor.getCount() > 0; cursor.close(); diff --git a/app/src/main/java/org/fdroid/fdroid/data/NewRepoConfig.java b/app/src/main/java/org/fdroid/fdroid/data/NewRepoConfig.java index c1242f345..8bf572b2c 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/NewRepoConfig.java +++ b/app/src/main/java/org/fdroid/fdroid/data/NewRepoConfig.java @@ -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(); diff --git a/app/src/main/java/org/fdroid/fdroid/data/Repo.java b/app/src/main/java/org/fdroid/fdroid/data/Repo.java index eafc06d78..348b19be6 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Repo.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Repo.java @@ -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,26 +305,43 @@ 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 getMirrorList() { + final ArrayList allMirrors = new ArrayList(); + 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) { - if (!m.equals(address)) { - if (FDroidApp.isUsingTor()) { + for (String m : getMirrorList()) { + if (!m.equals(address)) { + if (FDroidApp.isUsingTor()) { + count++; + } else { + if (!m.contains(".onion")) { count++; - } else { - if (!m.contains(".onion")) { - count++; - } } } } @@ -328,7 +353,7 @@ public class Repo extends ValueObject { if (TextUtils.isEmpty(lastWorkingMirror)) { lastWorkingMirror = address; } - List shuffledMirrors = Arrays.asList(mirrors); + List shuffledMirrors = getMirrorList(); Collections.shuffle(shuffledMirrors); if (shuffledMirrors.size() > 1) { for (String m : shuffledMirrors) { diff --git a/app/src/main/java/org/fdroid/fdroid/data/RepoPersister.java b/app/src/main/java/org/fdroid/fdroid/data/RepoPersister.java index f278de932..d8052b075 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/RepoPersister.java +++ b/app/src/main/java/org/fdroid/fdroid/data/RepoPersister.java @@ -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); } } diff --git a/app/src/main/java/org/fdroid/fdroid/data/Schema.java b/app/src/main/java/org/fdroid/fdroid/data/Schema.java index 9bcd11103..3642ebcd9 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Schema.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Schema.java @@ -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, }; } } diff --git a/app/src/main/java/org/fdroid/fdroid/net/BluetoothDownloader.java b/app/src/main/java/org/fdroid/fdroid/net/BluetoothDownloader.java index 8ab0c4704..85d4f4d84 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/BluetoothDownloader.java +++ b/app/src/main/java/org/fdroid/fdroid/net/BluetoothDownloader.java @@ -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; } diff --git a/app/src/main/java/org/fdroid/fdroid/net/Downloader.java b/app/src/main/java/org/fdroid/fdroid/net/Downloader.java index 885dc3d79..3dcb80479 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/Downloader.java +++ b/app/src/main/java/org/fdroid/fdroid/net/Downloader.java @@ -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); } } }; diff --git a/app/src/main/java/org/fdroid/fdroid/net/DownloaderFactory.java b/app/src/main/java/org/fdroid/fdroid/net/DownloaderFactory.java index 8e87b4347..3d124eed9 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/DownloaderFactory.java +++ b/app/src/main/java/org/fdroid/fdroid/net/DownloaderFactory.java @@ -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; diff --git a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java index ca63ec17c..cf564bab9 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java +++ b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java @@ -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) { diff --git a/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java b/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java index c44f94487..8dba014de 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java +++ b/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java @@ -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() { - return connection.getContentLength(); + @TargetApi(24) + public long totalDownloadSize() { + if (Build.VERSION.SDK_INT < 24) { + return connection.getContentLength(); + } else { + return connection.getContentLengthLong(); + } } @Override diff --git a/app/src/main/java/org/fdroid/fdroid/net/bluetooth/FileDetails.java b/app/src/main/java/org/fdroid/fdroid/net/bluetooth/FileDetails.java index f7148a91f..96c57f63c 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/bluetooth/FileDetails.java +++ b/app/src/main/java/org/fdroid/fdroid/net/bluetooth/FileDetails.java @@ -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; } diff --git a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java index bc470667f..0ce35a301 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java @@ -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, RepoAdapter.EnabledListener { +public class ManageReposActivity extends AppCompatActivity + implements LoaderManager.LoaderCallbacks, 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 urlRepoMap = new HashMap<>(); + private final HashMap 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: + *
    + *
  • a signed repo with a {@link Repo#address URL} and fingerprint that matches + *
  • a signed repo with a matching fingerprint and URL that matches a mirror + *
  • a signed repo with a matching fingerprint, but the URL doesn't match any known mirror + *
  • an unsigned repo and no fingerprint was supplied + *
*/ 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 checker = new AsyncTask() { 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(); diff --git a/app/src/main/java/org/fdroid/fdroid/views/RepoDetailsActivity.java b/app/src/main/java/org/fdroid/fdroid/views/RepoDetailsActivity.java index d12623aa1..f22785318 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/RepoDetailsActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/RepoDetailsActivity.java @@ -50,26 +50,26 @@ public class RepoDetailsActivity extends ActionBarActivity { * all of this info, otherwise they will be hidden. */ private static final int[] SHOW_IF_EXISTS = { - R.id.label_repo_name, - R.id.text_repo_name, - R.id.text_description, - R.id.label_num_apps, - R.id.text_num_apps, - R.id.label_last_update, - R.id.text_last_update, - R.id.label_username, - R.id.text_username, - R.id.button_edit_credentials, - R.id.label_repo_fingerprint, - R.id.text_repo_fingerprint, - R.id.text_repo_fingerprint_description, + R.id.label_repo_name, + R.id.text_repo_name, + R.id.text_description, + R.id.label_num_apps, + R.id.text_num_apps, + R.id.label_last_update, + R.id.text_last_update, + R.id.label_username, + R.id.text_username, + R.id.button_edit_credentials, + R.id.label_repo_fingerprint, + R.id.text_repo_fingerprint, + R.id.text_repo_fingerprint_description, }; /** * If the repo has not been updated yet, then we only show * these, otherwise they are hidden. */ private static final int[] HIDE_IF_EXISTS = { - R.id.text_not_yet_updated, + R.id.text_not_yet_updated, }; private Repo repo; private long repoId; @@ -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); @@ -345,22 +379,22 @@ public class RepoDetailsActivity extends ActionBarActivity { private void promptForDelete() { new AlertDialog.Builder(this) - .setTitle(R.string.repo_confirm_delete_title) - .setMessage(R.string.repo_confirm_delete_body) - .setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - RepoProvider.Helper.remove(getApplicationContext(), repoId); - finish(); - } - }).setNegativeButton(android.R.string.cancel, + .setTitle(R.string.repo_confirm_delete_title) + .setMessage(R.string.repo_confirm_delete_body) + .setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + RepoProvider.Helper.remove(getApplicationContext(), repoId); + finish(); + } + }).setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // Do nothing... } } - ).show(); + ).show(); } public void showChangePasswordDialog(final View parentView) { diff --git a/app/src/main/java/sun/net/www/protocol/bluetooth/Handler.java b/app/src/main/java/sun/net/www/protocol/bluetooth/Handler.java deleted file mode 100644 index 34234ee89..000000000 --- a/app/src/main/java/sun/net/www/protocol/bluetooth/Handler.java +++ /dev/null @@ -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"); - } -} diff --git a/app/src/main/res/layout/repodetails.xml b/app/src/main/res/layout/repodetails.xml index 8c3711c81..375db46ca 100644 --- a/app/src/main/res/layout/repodetails.xml +++ b/app/src/main/res/layout/repodetails.xml @@ -71,6 +71,30 @@ android:id="@+id/text_last_update" style="@style/BodyText" /> + + + + + + + + No
Add new repository Add + Add mirror Links Versions More @@ -134,12 +135,11 @@ This often occurs with apps installed via Google Play or other sources, if they Repository address Fingerprint (optional) - This repo is already setup, this will add new key information. - This repo is already setup, confirm that you want to re-enable it. - The incoming repo is already setup and enabled. - You must first delete this repo before you can add one with a different - key. - + %1$s is already setup, this will add new key information. + %1$s is already setup, confirm that you want to re-enable it. + %1$s is already setup and enabled. + First delete %1$s in order to add this with a conflicting key. + This is a copy of %1$s, add it as a mirror? Bad fingerprint This is not a valid URL. Ignoring malformed repo URI: %s @@ -307,6 +307,8 @@ This often occurs with apps installed via Google Play or other sources, if they Fingerprint of the signing key (SHA-256) Description Last update + Official mirrors + User mirrors Name This means that the list of apps could not be verified. You should be careful diff --git a/app/src/test/java/org/fdroid/fdroid/updater/MultiRepoUpdaterTest.java b/app/src/test/java/org/fdroid/fdroid/updater/MultiRepoUpdaterTest.java index f7782553d..09026171c 100644 --- a/app/src/test/java/org/fdroid/fdroid/updater/MultiRepoUpdaterTest.java +++ b/app/src/test/java/org/fdroid/fdroid/updater/MultiRepoUpdaterTest.java @@ -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) {