Merge branch 'clean-up-repo-and-downloader' into 'master'
expose mirror options; Clean up Repo and Downloader See merge request fdroid/fdroidclient!663
This commit is contained in:
commit
5f2b053b1c
@ -180,7 +180,7 @@ def preDexEnabled = "true".equals(System.getProperty("pre-dex", "true"))
|
||||
|
||||
android {
|
||||
compileSdkVersion 24
|
||||
buildToolsVersion '25.0.2'
|
||||
buildToolsVersion '25.0.3'
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
|
||||
buildTypes {
|
||||
|
@ -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
|
||||
|
@ -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!");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
|
@ -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!");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
|
@ -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<Repo> 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<Repo> 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();
|
||||
|
@ -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();
|
||||
|
@ -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<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) {
|
||||
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<String> shuffledMirrors = Arrays.asList(mirrors);
|
||||
List<String> shuffledMirrors = getMirrorList();
|
||||
Collections.shuffle(shuffledMirrors);
|
||||
if (shuffledMirrors.size() > 1) {
|
||||
for (String m : shuffledMirrors) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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 <em>not</em> 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) {
|
||||
|
@ -1,18 +0,0 @@
|
||||
package sun.net.www.protocol.bluetooth;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
|
||||
/**
|
||||
* This class is added so that the bluetooth:// scheme we use for the {@link
|
||||
* org.fdroid.fdroid.net.BluetoothDownloader} is not treated as invalid by
|
||||
* the {@link URL} class.
|
||||
*/
|
||||
public class Handler extends URLStreamHandler {
|
||||
@Override
|
||||
protected URLConnection openConnection(URL u) throws IOException {
|
||||
throw new UnsupportedOperationException("openConnection() not supported on bluetooth:// URLs");
|
||||
}
|
||||
}
|
@ -71,6 +71,30 @@
|
||||
android:id="@+id/text_last_update"
|
||||
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"
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user