WIP: Do repo update database work in temp table for apks, then copy at end.

At the start of a repo update, it will create a copy of the apk table.
Throughout the update, it will query the original apk table for info.
All inserts and updates happen to the temp table. After the repo has been
verified as trusted, the original apk table is emptied, and all apks are
copied from the temp table to the real one.

I realise that the work done to query the apk table for info during the update
could happen against the temp table, but it was not neccesary to move all of
the queries that are required for this task to the temp apk provider.
This commit is contained in:
Peter Serwylo 2015-09-05 10:12:12 +10:00
parent b989ef3ecc
commit 9a2d390279
4 changed files with 164 additions and 11 deletions

View File

@ -83,6 +83,11 @@
android:name="org.fdroid.fdroid.data.ApkProvider"
android:exported="false"/>
<provider
android:authorities="org.fdroid.fdroid.data.TempApkProvider"
android:name="org.fdroid.fdroid.data.TempApkProvider"
android:exported="false"/>
<provider
android:authorities="org.fdroid.fdroid.data.InstalledAppProvider"
android:name="org.fdroid.fdroid.data.InstalledAppProvider"

View File

@ -17,6 +17,7 @@ import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.data.TempApkProvider;
import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.DownloaderFactory;
import org.xml.sax.InputSource;
@ -225,7 +226,7 @@ public class RepoUpdater {
apkOperations.addAll(insertOrUpdateApks(apksToSaveList));
try {
context.getContentResolver().applyBatch(ApkProvider.getAuthority(), apkOperations);
context.getContentResolver().applyBatch(TempApkProvider.getAuthority(), apkOperations);
} catch (RemoteException | OperationApplicationException e) {
throw new UpdateException(repo, "An internal error occured while updating the database", e);
}
@ -325,7 +326,7 @@ public class RepoUpdater {
* <strong>Does not do any checks to see if the apk already exists or not.</strong>
*/
private ContentProviderOperation updateExistingApk(final Apk apk) {
Uri uri = ApkProvider.getContentUri(apk);
Uri uri = TempApkProvider.getApkUri(apk);
ContentValues values = apk.toContentValues();
return ContentProviderOperation.newUpdate(uri).withValues(values).build();
}
@ -336,7 +337,7 @@ public class RepoUpdater {
*/
private ContentProviderOperation insertNewApk(final Apk apk) {
ContentValues values = apk.toContentValues();
Uri uri = ApkProvider.getContentUri();
Uri uri = TempApkProvider.getContentUri();
return ContentProviderOperation.newInsert(uri).withValues(values).build();
}
@ -376,7 +377,7 @@ public class RepoUpdater {
// TODO: Deal with more than MAX_QUERY_PARAMS...
if (toDelete.size() > 0) {
Uri uri = ApkProvider.getContentUriForApks(repo, toDelete);
Uri uri = TempApkProvider.getApksUri(repo, toDelete);
return ContentProviderOperation.newDelete(uri).build();
} else {
return null;
@ -409,6 +410,13 @@ public class RepoUpdater {
if (downloadedFile == null || !downloadedFile.exists())
throw new UpdateException(repo, downloadedFile + " does not exist!");
// This is where we will store all of the metadata before commiting at the
// end of the process. This is due to the fact that we can't verify the cert
// the index was signed with until we've finished reading it - and we don't
// want to put stuff in the real database until we are sure it is from a
// trusted source.
TempApkProvider.Helper.init(context);
// Due to a bug in Android 5.0 Lollipop, the inclusion of spongycastle causes
// breakage when verifying the signature of the downloaded .jar. For more
// details, check out https://gitlab.com/fdroid/fdroidclient/issues/111.
@ -419,9 +427,6 @@ public class RepoUpdater {
indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry),
progressListener, repo, (int) indexEntry.getSize());
/* JarEntry can only read certificates after the file represented by that JarEntry
* has been read completely, so verification cannot run until now... */
// Process the index...
final SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
final XMLReader reader = parser.getXMLReader();
@ -430,7 +435,11 @@ public class RepoUpdater {
reader.parse(new InputSource(indexInputStream));
signingCertFromJar = getSigningCertFromJar(indexEntry);
// JarEntry can only read certificates after the file represented by that JarEntry
// has been read completely, so verification cannot run until now...
assertSigningCertFromXmlCorrect();
TempApkProvider.Helper.commit(context);
RepoProvider.Helper.update(context, repo, repoDetailsToSave);
} catch (SAXException | ParserConfigurationException | IOException e) {

View File

@ -227,15 +227,15 @@ public class ApkProvider extends FDroidProvider {
private static final int CODE_REPO = CODE_APP + 1;
private static final int CODE_APKS = CODE_REPO + 1;
private static final int CODE_REPO_APPS = CODE_APKS + 1;
private static final int CODE_REPO_APK = CODE_REPO_APPS + 1;
protected static final int CODE_REPO_APK = CODE_REPO_APPS + 1;
private static final String PROVIDER_NAME = "ApkProvider";
private static final String PATH_APK = "apk";
protected static final String PATH_APK = "apk";
private static final String PATH_APKS = "apks";
private static final String PATH_APP = "app";
private static final String PATH_REPO = "repo";
private static final String PATH_REPO_APPS = "repo-apps";
private static final String PATH_REPO_APK = "repo-apk";
protected static final String PATH_REPO_APK = "repo-apk";
private static final UriMatcher matcher = new UriMatcher(-1);
@ -322,7 +322,7 @@ public class ApkProvider extends FDroidProvider {
.build();
}
private static String buildApkString(List<Apk> apks) {
protected static String buildApkString(List<Apk> apks) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < apks.size(); i++) {
if (i != 0) {

View File

@ -0,0 +1,139 @@
package org.fdroid.fdroid.data;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.provider.BaseColumns;
import android.util.Log;
import org.fdroid.fdroid.Utils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* This class does all of its operations in a temporary sqlite table.
*/
public class TempApkProvider extends ApkProvider {
private static final String TAG = "TempApkProvider";
private static final String PROVIDER_NAME = "TempApkProvider";
private static final String PATH_INIT = "init";
private static final String PATH_COMMIT = "commit";
private static final String PATH_ROLLBACK = "rollback";
private static final int CODE_INIT = 10000;
private static final int CODE_COMMIT = CODE_INIT + 1;
private static final int CODE_ROLLBACK = CODE_INIT + 2;
private static final UriMatcher matcher = new UriMatcher(-1);
static {
matcher.addURI(getAuthority(), PATH_INIT, CODE_INIT);
matcher.addURI(getAuthority(), PATH_COMMIT, CODE_COMMIT);
matcher.addURI(getAuthority(), PATH_ROLLBACK, CODE_ROLLBACK);
matcher.addURI(getAuthority(), PATH_APK + "/#/*", CODE_SINGLE);
matcher.addURI(getAuthority(), PATH_REPO_APK + "/#/*", CODE_REPO_APK);
}
@Override
protected String getTableName() {
return "temp_" + super.getTableName();
}
public static String getAuthority() {
return AUTHORITY + "." + PROVIDER_NAME;
}
public static Uri getContentUri() {
return Uri.parse("content://" + getAuthority());
}
public static Uri getApkUri(Apk apk) {
return getContentUri()
.buildUpon()
.appendPath(PATH_APK)
.appendPath(Integer.toString(apk.vercode))
.appendPath(apk.id)
.build();
}
public static Uri getApksUri(Repo repo, List<Apk> apks) {
return getContentUri()
.buildUpon()
.appendPath(PATH_REPO_APK)
.appendPath(Long.toString(repo.id))
.appendPath(buildApkString(apks))
.build();
}
public static class Helper {
/**
* Deletes the old temporary table (if it exists). Then creates a new temporary apk provider
* table and populates it with all the data from the real apk provider table.
*/
public static void init(Context context) {
Uri uri = Uri.withAppendedPath(getContentUri(), PATH_INIT);
context.getContentResolver().insert(uri, new ContentValues());
}
/**
* Saves data from the temp table to the apk table, by removing _EVERYTHING_ from the real
* apk table and inserting all of the records from here. The temporary table is then removed.
*/
public static void commit(Context context) {
Uri uri = Uri.withAppendedPath(getContentUri(), PATH_COMMIT);
context.getContentResolver().insert(uri, new ContentValues());
}
/**
* Not sure that this is strictly necessary, but this will remove the temp table.
*/
public static void rollback(Context context) {
Uri uri = Uri.withAppendedPath(getContentUri(), PATH_ROLLBACK);
context.getContentResolver().insert(uri, new ContentValues());
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
int code = matcher.match(uri);
if (code == CODE_INIT) {
initTable();
return null;
} else if (code == CODE_COMMIT) {
commitTable();
return null;
} else if (code == CODE_ROLLBACK) {
removeTable();
return null;
} else {
return super.insert(uri, values);
}
}
private void initTable() {
removeTable();
write().execSQL("CREATE TEMPORARY TABLE " + getTableName() + " AS SELECT * FROM " + DBHelper.TABLE_APK);
}
private void commitTable() {
Log.d(TAG, "Deleting all apks from " + DBHelper.TABLE_APK + " so they can be copied from " + getTableName());
write().execSQL("DELETE FROM " + DBHelper.TABLE_APK);
write().execSQL("INSERT INTO " + DBHelper.TABLE_APK + " SELECT * FROM " + getTableName());
}
private void removeTable() {
write().execSQL("DROP TABLE IF EXISTS " + getTableName());
}
}