From b6218c6d056e9baaf9ae4422c3574f7e63ff7ba9 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Tue, 3 Nov 2015 00:57:51 +1100
Subject: [PATCH 1/7] Refactored code which inserts apps into database to make
 testable.

Putting it in the UpdateService made it a little tricky to test, so
I moved it out to a separate class called `RepoPersister`.
---
 .../src/org/fdroid/fdroid/RepoPersister.java  | 329 ++++++++++++++++++
 .../src/org/fdroid/fdroid/UpdateService.java  | 303 +---------------
 2 files changed, 344 insertions(+), 288 deletions(-)
 create mode 100644 F-Droid/src/org/fdroid/fdroid/RepoPersister.java

diff --git a/F-Droid/src/org/fdroid/fdroid/RepoPersister.java b/F-Droid/src/org/fdroid/fdroid/RepoPersister.java
new file mode 100644
index 000000000..3f82422be
--- /dev/null
+++ b/F-Droid/src/org/fdroid/fdroid/RepoPersister.java
@@ -0,0 +1,329 @@
+package org.fdroid.fdroid;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.support.annotation.NonNull;
+import android.util.Log;
+
+import org.fdroid.fdroid.data.Apk;
+import org.fdroid.fdroid.data.ApkProvider;
+import org.fdroid.fdroid.data.App;
+import org.fdroid.fdroid.data.AppProvider;
+import org.fdroid.fdroid.data.Repo;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Saves app and apk information to the database after a {@link RepoUpdater} has processed the
+ * relevant index file.
+ */
+public class RepoPersister {
+
+    private static final String TAG = "RepoPersister";
+
+    /**
+     * When an app already exists in the db, and we are updating it on the off chance that some
+     * values changed in the index, some fields should not be updated. Rather, they should be
+     * ignored, because they were explicitly set by the user, and hence can't be automatically
+     * overridden by the index.
+     *
+     * NOTE: In the future, these attributes will be moved to a join table, so that the app table
+     * is essentially completely transient, and can be nuked at any time.
+     */
+    private static final String[] APP_FIELDS_TO_IGNORE = {
+            AppProvider.DataColumns.IGNORE_ALLUPDATES,
+            AppProvider.DataColumns.IGNORE_THISUPDATE,
+    };
+
+    @NonNull
+    private final Context context;
+
+    private Map<String, App> appsToUpdate = new HashMap<>();
+    private List<Apk> apksToUpdate = new ArrayList<>();
+    private List<Repo> repos = new ArrayList<>();
+
+    public RepoPersister(@NonNull Context context) {
+        this.context = context;
+    }
+
+    public RepoPersister queueUpdater(RepoUpdater updater) {
+        queueApps(updater.getApps());
+        queueApks(updater.getApks());
+        repos.add(updater.repo);
+        return this;
+    }
+
+    private void queueApps(List<App> apps) {
+        for (final App app : apps) {
+            appsToUpdate.put(app.id, app);
+        }
+    }
+
+    private void queueApks(List<Apk> apks) {
+        apksToUpdate.addAll(apks);
+    }
+
+    public void save(List<Repo> disabledRepos) {
+
+        List<App> listOfAppsToUpdate = new ArrayList<>();
+        listOfAppsToUpdate.addAll(appsToUpdate.values());
+
+        calcApkCompatibilityFlags(apksToUpdate);
+
+        // Need to do this BEFORE updating the apks, otherwise when it continually
+        // calls "get existing apks for repo X" then it will be getting the newly
+        // created apks, rather than those from the fresh, juicy index we just processed.
+        removeApksNoLongerInRepo(apksToUpdate, repos);
+
+        int totalInsertsUpdates = listOfAppsToUpdate.size() + apksToUpdate.size();
+        updateOrInsertApps(listOfAppsToUpdate, totalInsertsUpdates, 0);
+        updateOrInsertApks(apksToUpdate, totalInsertsUpdates, listOfAppsToUpdate.size());
+        removeApksFromRepos(disabledRepos);
+        removeAppsWithoutApks();
+
+        // This will sort out the icon urls, compatibility flags. and suggested version
+        // for each app. It used to happen here in Java code, but was moved to SQL when
+        // it became apparant we don't always have enough info (depending on which repos
+        // were updated).
+        AppProvider.Helper.calcDetailsFromIndex(context);
+
+    }
+
+    /**
+     * This cannot be offloaded to the database (as we did with the query which
+     * updates apps, depending on whether their apks are compatible or not).
+     * The reason is that we need to interact with the CompatibilityChecker
+     * in order to see if, and why an apk is not compatible.
+     */
+    private void calcApkCompatibilityFlags(List<Apk> apks) {
+        final CompatibilityChecker checker = new CompatibilityChecker(context);
+        for (final Apk apk : apks) {
+            final List<String> reasons = checker.getIncompatibleReasons(apk);
+            if (reasons.size() > 0) {
+                apk.compatible = false;
+                apk.incompatibleReasons = Utils.CommaSeparatedList.make(reasons);
+            } else {
+                apk.compatible = true;
+                apk.incompatibleReasons = null;
+            }
+        }
+    }
+
+    /**
+     * If a repo was updated (i.e. it is in use, and the index has changed
+     * since last time we did an update), then we want to remove any apks that
+     * belong to the repo which are not in the current list of apks that were
+     * retrieved.
+     */
+    private void removeApksNoLongerInRepo(List<Apk> apksToUpdate, List<Repo> updatedRepos) {
+
+        long startTime = System.currentTimeMillis();
+        List<Apk> toRemove = new ArrayList<>();
+
+        final String[] fields = {
+                ApkProvider.DataColumns.APK_ID,
+                ApkProvider.DataColumns.VERSION_CODE,
+                ApkProvider.DataColumns.VERSION,
+        };
+
+        for (final Repo repo : updatedRepos) {
+            final List<Apk> existingApks = ApkProvider.Helper.findByRepo(context, repo, fields);
+            for (final Apk existingApk : existingApks) {
+                if (!isApkToBeUpdated(existingApk, apksToUpdate)) {
+                    toRemove.add(existingApk);
+                }
+            }
+        }
+
+        long duration = System.currentTimeMillis() - startTime;
+        Utils.debugLog(TAG, "Found " + toRemove.size() + " apks no longer in the updated repos (took " + duration + "ms)");
+
+        if (toRemove.size() > 0) {
+            ApkProvider.Helper.deleteApks(context, toRemove);
+        }
+    }
+
+    private void updateOrInsertApps(List<App> appsToUpdate, int totalUpdateCount, int currentCount) {
+
+        List<ContentProviderOperation> operations = new ArrayList<>();
+        List<String> knownAppIds = getKnownAppIds(appsToUpdate);
+        for (final App app : appsToUpdate) {
+            if (knownAppIds.contains(app.id)) {
+                operations.add(updateExistingApp(app));
+            } else {
+                operations.add(insertNewApp(app));
+            }
+        }
+
+        Utils.debugLog(TAG, "Updating/inserting " + operations.size() + " apps.");
+        try {
+            executeBatchWithStatus(AppProvider.getAuthority(), operations, currentCount, totalUpdateCount);
+        } catch (RemoteException | OperationApplicationException e) {
+            Log.e(TAG, "Could not update or insert apps", e);
+        }
+    }
+
+    private void executeBatchWithStatus(String providerAuthority,
+                                        List<ContentProviderOperation> operations,
+                                        int currentCount,
+                                        int totalUpdateCount)
+            throws RemoteException, OperationApplicationException {
+        int i = 0;
+        while (i < operations.size()) {
+            int count = Math.min(operations.size() - i, 100);
+            ArrayList<ContentProviderOperation> o = new ArrayList<>(operations.subList(i, i + count));
+            UpdateService.sendStatus(context, UpdateService.STATUS_INFO, context.getString(
+                    R.string.status_inserting,
+                    (int) ((double) (currentCount + i) / totalUpdateCount * 100)));
+            context.getContentResolver().applyBatch(providerAuthority, o);
+            i += 100;
+        }
+    }
+
+    /**
+     * Return list of apps from the "apks" argument which are already in the database.
+     */
+    private List<Apk> getKnownApks(List<Apk> apks) {
+        final String[] fields = {
+                ApkProvider.DataColumns.APK_ID,
+                ApkProvider.DataColumns.VERSION,
+                ApkProvider.DataColumns.VERSION_CODE,
+        };
+        return ApkProvider.Helper.knownApks(context, apks, fields);
+    }
+
+    private void updateOrInsertApks(List<Apk> apksToUpdate, int totalApksAppsCount, int currentCount) {
+
+        List<ContentProviderOperation> operations = new ArrayList<>();
+
+        List<Apk> knownApks = getKnownApks(apksToUpdate);
+        for (final Apk apk : apksToUpdate) {
+            boolean known = false;
+            for (final Apk knownApk : knownApks) {
+                if (knownApk.id.equals(apk.id) && knownApk.vercode == apk.vercode) {
+                    known = true;
+                    break;
+                }
+            }
+
+            if (known) {
+                operations.add(updateExistingApk(apk));
+            } else {
+                operations.add(insertNewApk(apk));
+                knownApks.add(apk); // In case another repo has the same version/id combo for this apk.
+            }
+        }
+
+        Utils.debugLog(TAG, "Updating/inserting " + operations.size() + " apks.");
+        try {
+            executeBatchWithStatus(ApkProvider.getAuthority(), operations, currentCount, totalApksAppsCount);
+        } catch (RemoteException | OperationApplicationException e) {
+            Log.e(TAG, "Could not update/insert apps", e);
+        }
+    }
+
+    private ContentProviderOperation updateExistingApk(final Apk apk) {
+        Uri uri = ApkProvider.getContentUri(apk);
+        ContentValues values = apk.toContentValues();
+        return ContentProviderOperation.newUpdate(uri).withValues(values).build();
+    }
+
+    private ContentProviderOperation insertNewApk(final Apk apk) {
+        ContentValues values = apk.toContentValues();
+        Uri uri = ApkProvider.getContentUri();
+        return ContentProviderOperation.newInsert(uri).withValues(values).build();
+    }
+
+    private ContentProviderOperation updateExistingApp(App app) {
+        Uri uri = AppProvider.getContentUri(app);
+        ContentValues values = app.toContentValues();
+        for (final String toIgnore : APP_FIELDS_TO_IGNORE) {
+            if (values.containsKey(toIgnore)) {
+                values.remove(toIgnore);
+            }
+        }
+        return ContentProviderOperation.newUpdate(uri).withValues(values).build();
+    }
+
+    private ContentProviderOperation insertNewApp(App app) {
+        ContentValues values = app.toContentValues();
+        Uri uri = AppProvider.getContentUri();
+        return ContentProviderOperation.newInsert(uri).withValues(values).build();
+    }
+
+    private static boolean isApkToBeUpdated(Apk existingApk, List<Apk> apksToUpdate) {
+        for (final Apk apkToUpdate : apksToUpdate) {
+            if (apkToUpdate.vercode == existingApk.vercode && apkToUpdate.id.equals(existingApk.id)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void removeApksFromRepos(List<Repo> repos) {
+        for (final Repo repo : repos) {
+            Uri uri = ApkProvider.getRepoUri(repo.getId());
+            int numDeleted = context.getContentResolver().delete(uri, null, null);
+            Utils.debugLog(TAG, "Removing " + numDeleted + " apks from repo " + repo.address);
+        }
+    }
+
+    private void removeAppsWithoutApks() {
+        int numDeleted = context.getContentResolver().delete(AppProvider.getNoApksUri(), null, null);
+        Utils.debugLog(TAG, "Removing " + numDeleted + " apks that don't have any apks");
+    }
+
+    private List<String> getKnownAppIds(List<App> apps) {
+        List<String> knownAppIds = new ArrayList<>();
+        if (apps.isEmpty()) {
+            return knownAppIds;
+        }
+        if (apps.size() > AppProvider.MAX_APPS_TO_QUERY) {
+            int middle = apps.size() / 2;
+            List<App> apps1 = apps.subList(0, middle);
+            List<App> apps2 = apps.subList(middle, apps.size());
+            knownAppIds.addAll(getKnownAppIds(apps1));
+            knownAppIds.addAll(getKnownAppIds(apps2));
+        } else {
+            knownAppIds.addAll(getKnownAppIdsFromProvider(apps));
+        }
+        return knownAppIds;
+    }
+
+    /**
+     * Looks in the database to see which apps we already know about. Only
+     * returns ids of apps that are in the database if they are in the "apps"
+     * array.
+     */
+    private List<String> getKnownAppIdsFromProvider(List<App> apps) {
+
+        final Uri uri = AppProvider.getContentUri(apps);
+        final String[] fields = {AppProvider.DataColumns.APP_ID};
+        Cursor cursor = context.getContentResolver().query(uri, fields, null, null, null);
+
+        int knownIdCount = cursor != null ? cursor.getCount() : 0;
+        List<String> knownIds = new ArrayList<>(knownIdCount);
+        if (cursor != null) {
+            if (knownIdCount > 0) {
+                cursor.moveToFirst();
+                while (!cursor.isAfterLast()) {
+                    knownIds.add(cursor.getString(0));
+                    cursor.moveToNext();
+                }
+            }
+            cursor.close();
+        }
+
+        return knownIds;
+    }
+
+}
+
diff --git a/F-Droid/src/org/fdroid/fdroid/UpdateService.java b/F-Droid/src/org/fdroid/fdroid/UpdateService.java
index 06872e5c7..9556523cd 100644
--- a/F-Droid/src/org/fdroid/fdroid/UpdateService.java
+++ b/F-Droid/src/org/fdroid/fdroid/UpdateService.java
@@ -23,19 +23,14 @@ import android.app.IntentService;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
-import android.content.ContentProviderOperation;
-import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.OperationApplicationException;
 import android.content.SharedPreferences;
 import android.database.Cursor;
 import android.net.ConnectivityManager;
 import android.net.NetworkInfo;
-import android.net.Uri;
 import android.os.Build;
-import android.os.RemoteException;
 import android.os.SystemClock;
 import android.preference.PreferenceManager;
 import android.support.v4.app.NotificationCompat;
@@ -45,7 +40,6 @@ import android.text.TextUtils;
 import android.util.Log;
 import android.widget.Toast;
 
-import org.fdroid.fdroid.data.Apk;
 import org.fdroid.fdroid.data.ApkProvider;
 import org.fdroid.fdroid.data.App;
 import org.fdroid.fdroid.data.AppProvider;
@@ -54,9 +48,7 @@ import org.fdroid.fdroid.data.RepoProvider;
 import org.fdroid.fdroid.net.Downloader;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
 public class UpdateService extends IntentService implements ProgressListener {
 
@@ -89,20 +81,6 @@ public class UpdateService extends IntentService implements ProgressListener {
         super("UpdateService");
     }
 
-    /**
-     * When an app already exists in the db, and we are updating it on the off chance that some
-     * values changed in the index, some fields should not be updated. Rather, they should be
-     * ignored, because they were explicitly set by the user, and hence can't be automatically
-     * overridden by the index.
-     *
-     * NOTE: In the future, these attributes will be moved to a join table, so that the app table
-     * is essentially completely transient, and can be nuked at any time.
-     */
-    private static final String[] APP_FIELDS_TO_IGNORE = {
-        AppProvider.DataColumns.IGNORE_ALLUPDATES,
-        AppProvider.DataColumns.IGNORE_THISUPDATE,
-    };
-
     public static void updateNow(Context context) {
         updateRepoNow(null, context);
     }
@@ -178,16 +156,16 @@ public class UpdateService extends IntentService implements ProgressListener {
         localBroadcastManager.unregisterReceiver(updateStatusReceiver);
     }
 
-    protected void sendStatus(int statusCode) {
-        sendStatus(statusCode, null);
+    protected static void sendStatus(Context context, int statusCode) {
+        sendStatus(context, statusCode, null);
     }
 
-    protected void sendStatus(int statusCode, String message) {
+    protected static void sendStatus(Context context, int statusCode, String message) {
         Intent intent = new Intent(LOCAL_ACTION_STATUS);
         intent.putExtra(EXTRA_STATUS_CODE, statusCode);
         if (!TextUtils.isEmpty(message))
             intent.putExtra(EXTRA_MESSAGE, message);
-        LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
+        LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
     }
 
     protected void sendRepoErrorStatus(int statusCode, ArrayList<CharSequence> repoErrors) {
@@ -219,7 +197,7 @@ public class UpdateService extends IntentService implements ProgressListener {
                 String totalSizeFriendly = Utils.getFriendlySize(totalSize);
                 message = getString(R.string.status_download, repoAddress, downloadedSizeFriendly, totalSizeFriendly, percent);
             }
-            sendStatus(STATUS_INFO, message);
+            sendStatus(context, STATUS_INFO, message);
         }
     };
 
@@ -354,8 +332,8 @@ public class UpdateService extends IntentService implements ProgressListener {
             List<Repo> repos = RepoProvider.Helper.all(this);
 
             // Process each repo...
-            Map<String, App> appsToUpdate = new HashMap<>();
-            List<Apk> apksToUpdate = new ArrayList<>();
+            RepoPersister appSaver = new RepoPersister(this);
+
             //List<Repo> swapRepos = new ArrayList<>();
             List<Repo> unchangedRepos = new ArrayList<>();
             List<Repo> updatedRepos = new ArrayList<>();
@@ -380,16 +358,13 @@ public class UpdateService extends IntentService implements ProgressListener {
                     continue;
                 }
 
-                sendStatus(STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address));
+                sendStatus(this, STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address));
                 RepoUpdater updater = new RepoUpdater(getBaseContext(), repo);
                 updater.setProgressListener(this);
                 try {
                     updater.update();
                     if (updater.hasChanged()) {
-                        for (final App app : updater.getApps()) {
-                            appsToUpdate.put(app.id, app);
-                        }
-                        apksToUpdate.addAll(updater.getApks());
+                        appSaver.queueUpdater(updater);
                         updatedRepos.add(repo);
                         changes = true;
                         repoUpdateRememberers.add(updater.getRememberer());
@@ -406,29 +381,9 @@ public class UpdateService extends IntentService implements ProgressListener {
             if (!changes) {
                 Utils.debugLog(TAG, "Not checking app details or compatibility, because all repos were up to date.");
             } else {
-                sendStatus(STATUS_INFO, getString(R.string.status_checking_compatibility));
+                sendStatus(this, STATUS_INFO, getString(R.string.status_checking_compatibility));
 
-                List<App> listOfAppsToUpdate = new ArrayList<>();
-                listOfAppsToUpdate.addAll(appsToUpdate.values());
-
-                calcApkCompatibilityFlags(this, apksToUpdate);
-
-                // Need to do this BEFORE updating the apks, otherwise when it continually
-                // calls "get existing apks for repo X" then it will be getting the newly
-                // created apks, rather than those from the fresh, juicy index we just processed.
-                removeApksNoLongerInRepo(apksToUpdate, updatedRepos);
-
-                int totalInsertsUpdates = listOfAppsToUpdate.size() + apksToUpdate.size();
-                updateOrInsertApps(listOfAppsToUpdate, totalInsertsUpdates, 0);
-                updateOrInsertApks(apksToUpdate, totalInsertsUpdates, listOfAppsToUpdate.size());
-                removeApksFromRepos(disabledRepos);
-                removeAppsWithoutApks();
-
-                // This will sort out the icon urls, compatibility flags. and suggested version
-                // for each app. It used to happen here in Java code, but was moved to SQL when
-                // it became apparant we don't always have enough info (depending on which repos
-                // were updated).
-                AppProvider.Helper.calcDetailsFromIndex(this);
+                appSaver.save(disabledRepos);
 
                 notifyContentProviders();
 
@@ -448,9 +403,9 @@ public class UpdateService extends IntentService implements ProgressListener {
 
             if (errorRepos.isEmpty()) {
                 if (changes) {
-                    sendStatus(STATUS_COMPLETE_WITH_CHANGES);
+                    sendStatus(this, STATUS_COMPLETE_WITH_CHANGES);
                 } else {
-                    sendStatus(STATUS_COMPLETE_AND_SAME);
+                    sendStatus(this, STATUS_COMPLETE_AND_SAME);
                 }
             } else {
                 if (updatedRepos.size() + unchangedRepos.size() == 0) {
@@ -461,7 +416,7 @@ public class UpdateService extends IntentService implements ProgressListener {
             }
         } catch (Exception e) {
             Log.e(TAG, "Exception during update processing", e);
-            sendStatus(STATUS_ERROR_GLOBAL, e.getMessage());
+            sendStatus(this, STATUS_ERROR_GLOBAL, e.getMessage());
         }
     }
 
@@ -470,26 +425,6 @@ public class UpdateService extends IntentService implements ProgressListener {
         getContentResolver().notifyChange(ApkProvider.getContentUri(), null);
     }
 
-    /**
-     * This cannot be offloaded to the database (as we did with the query which
-     * updates apps, depending on whether their apks are compatible or not).
-     * The reason is that we need to interact with the CompatibilityChecker
-     * in order to see if, and why an apk is not compatible.
-     */
-    private static void calcApkCompatibilityFlags(Context context, List<Apk> apks) {
-        final CompatibilityChecker checker = new CompatibilityChecker(context);
-        for (final Apk apk : apks) {
-            final List<String> reasons = checker.getIncompatibleReasons(apk);
-            if (reasons.size() > 0) {
-                apk.compatible = false;
-                apk.incompatibleReasons = Utils.CommaSeparatedList.make(reasons);
-            } else {
-                apk.compatible = true;
-                apk.incompatibleReasons = null;
-            }
-        }
-    }
-
     private void performUpdateNotification() {
         Cursor cursor = getContentResolver().query(
                 AppProvider.getCanUpdateUri(),
@@ -557,214 +492,6 @@ public class UpdateService extends IntentService implements ProgressListener {
         notificationManager.notify(NOTIFY_ID_UPDATES_AVAILABLE, builder.build());
     }
 
-    private List<String> getKnownAppIds(List<App> apps) {
-        List<String> knownAppIds = new ArrayList<>();
-        if (apps.isEmpty()) {
-            return knownAppIds;
-        }
-        if (apps.size() > AppProvider.MAX_APPS_TO_QUERY) {
-            int middle = apps.size() / 2;
-            List<App> apps1 = apps.subList(0, middle);
-            List<App> apps2 = apps.subList(middle, apps.size());
-            knownAppIds.addAll(getKnownAppIds(apps1));
-            knownAppIds.addAll(getKnownAppIds(apps2));
-        } else {
-            knownAppIds.addAll(getKnownAppIdsFromProvider(apps));
-        }
-        return knownAppIds;
-    }
-
-    /**
-     * Looks in the database to see which apps we already know about. Only
-     * returns ids of apps that are in the database if they are in the "apps"
-     * array.
-     */
-    private List<String> getKnownAppIdsFromProvider(List<App> apps) {
-
-        final Uri uri = AppProvider.getContentUri(apps);
-        final String[] fields = {AppProvider.DataColumns.APP_ID};
-        Cursor cursor = getContentResolver().query(uri, fields, null, null, null);
-
-        int knownIdCount = cursor != null ? cursor.getCount() : 0;
-        List<String> knownIds = new ArrayList<>(knownIdCount);
-        if (cursor != null) {
-            if (knownIdCount > 0) {
-                cursor.moveToFirst();
-                while (!cursor.isAfterLast()) {
-                    knownIds.add(cursor.getString(0));
-                    cursor.moveToNext();
-                }
-            }
-            cursor.close();
-        }
-
-        return knownIds;
-    }
-
-    private void updateOrInsertApps(List<App> appsToUpdate, int totalUpdateCount, int currentCount) {
-
-        List<ContentProviderOperation> operations = new ArrayList<>();
-        List<String> knownAppIds = getKnownAppIds(appsToUpdate);
-        for (final App app : appsToUpdate) {
-            if (knownAppIds.contains(app.id)) {
-                operations.add(updateExistingApp(app));
-            } else {
-                operations.add(insertNewApp(app));
-            }
-        }
-
-        Utils.debugLog(TAG, "Updating/inserting " + operations.size() + " apps.");
-        try {
-            executeBatchWithStatus(AppProvider.getAuthority(), operations, currentCount, totalUpdateCount);
-        } catch (RemoteException | OperationApplicationException e) {
-            Log.e(TAG, "Could not update or insert apps", e);
-        }
-    }
-
-    private void executeBatchWithStatus(String providerAuthority,
-                                        List<ContentProviderOperation> operations,
-                                        int currentCount,
-                                        int totalUpdateCount)
-            throws RemoteException, OperationApplicationException {
-        int i = 0;
-        while (i < operations.size()) {
-            int count = Math.min(operations.size() - i, 100);
-            ArrayList<ContentProviderOperation> o = new ArrayList<>(operations.subList(i, i + count));
-            sendStatus(STATUS_INFO, getString(
-                R.string.status_inserting,
-                (int) ((double) (currentCount + i) / totalUpdateCount * 100)));
-            getContentResolver().applyBatch(providerAuthority, o);
-            i += 100;
-        }
-    }
-
-    /**
-     * Return list of apps from the "apks" argument which are already in the database.
-     */
-    private List<Apk> getKnownApks(List<Apk> apks) {
-        final String[] fields = {
-            ApkProvider.DataColumns.APK_ID,
-            ApkProvider.DataColumns.VERSION,
-            ApkProvider.DataColumns.VERSION_CODE,
-        };
-        return ApkProvider.Helper.knownApks(this, apks, fields);
-    }
-
-    private void updateOrInsertApks(List<Apk> apksToUpdate, int totalApksAppsCount, int currentCount) {
-
-        List<ContentProviderOperation> operations = new ArrayList<>();
-
-        List<Apk> knownApks = getKnownApks(apksToUpdate);
-        for (final Apk apk : apksToUpdate) {
-            boolean known = false;
-            for (final Apk knownApk : knownApks) {
-                if (knownApk.id.equals(apk.id) && knownApk.vercode == apk.vercode) {
-                    known = true;
-                    break;
-                }
-            }
-
-            if (known) {
-                operations.add(updateExistingApk(apk));
-            } else {
-                operations.add(insertNewApk(apk));
-                knownApks.add(apk); // In case another repo has the same version/id combo for this apk.
-            }
-        }
-
-        Utils.debugLog(TAG, "Updating/inserting " + operations.size() + " apks.");
-        try {
-            executeBatchWithStatus(ApkProvider.getAuthority(), operations, currentCount, totalApksAppsCount);
-        } catch (RemoteException | OperationApplicationException e) {
-            Log.e(TAG, "Could not update/insert apps", e);
-        }
-    }
-
-    private ContentProviderOperation updateExistingApk(final Apk apk) {
-        Uri uri = ApkProvider.getContentUri(apk);
-        ContentValues values = apk.toContentValues();
-        return ContentProviderOperation.newUpdate(uri).withValues(values).build();
-    }
-
-    private ContentProviderOperation insertNewApk(final Apk apk) {
-        ContentValues values = apk.toContentValues();
-        Uri uri = ApkProvider.getContentUri();
-        return ContentProviderOperation.newInsert(uri).withValues(values).build();
-    }
-
-    private ContentProviderOperation updateExistingApp(App app) {
-        Uri uri = AppProvider.getContentUri(app);
-        ContentValues values = app.toContentValues();
-        for (final String toIgnore : APP_FIELDS_TO_IGNORE) {
-            if (values.containsKey(toIgnore)) {
-                values.remove(toIgnore);
-            }
-        }
-        return ContentProviderOperation.newUpdate(uri).withValues(values).build();
-    }
-
-    private ContentProviderOperation insertNewApp(App app) {
-        ContentValues values = app.toContentValues();
-        Uri uri = AppProvider.getContentUri();
-        return ContentProviderOperation.newInsert(uri).withValues(values).build();
-    }
-
-    /**
-     * If a repo was updated (i.e. it is in use, and the index has changed
-     * since last time we did an update), then we want to remove any apks that
-     * belong to the repo which are not in the current list of apks that were
-     * retrieved.
-     */
-    private void removeApksNoLongerInRepo(List<Apk> apksToUpdate, List<Repo> updatedRepos) {
-
-        long startTime = System.currentTimeMillis();
-        List<Apk> toRemove = new ArrayList<>();
-
-        final String[] fields = {
-            ApkProvider.DataColumns.APK_ID,
-            ApkProvider.DataColumns.VERSION_CODE,
-            ApkProvider.DataColumns.VERSION,
-        };
-
-        for (final Repo repo : updatedRepos) {
-            final List<Apk> existingApks = ApkProvider.Helper.findByRepo(this, repo, fields);
-            for (final Apk existingApk : existingApks) {
-                if (!isApkToBeUpdated(existingApk, apksToUpdate)) {
-                    toRemove.add(existingApk);
-                }
-            }
-        }
-
-        long duration = System.currentTimeMillis() - startTime;
-        Utils.debugLog(TAG, "Found " + toRemove.size() + " apks no longer in the updated repos (took " + duration + "ms)");
-
-        if (toRemove.size() > 0) {
-            ApkProvider.Helper.deleteApks(this, toRemove);
-        }
-    }
-
-    private static boolean isApkToBeUpdated(Apk existingApk, List<Apk> apksToUpdate) {
-        for (final Apk apkToUpdate : apksToUpdate) {
-            if (apkToUpdate.vercode == existingApk.vercode && apkToUpdate.id.equals(existingApk.id)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private void removeApksFromRepos(List<Repo> repos) {
-        for (final Repo repo : repos) {
-            Uri uri = ApkProvider.getRepoUri(repo.getId());
-            int numDeleted = getContentResolver().delete(uri, null, null);
-            Utils.debugLog(TAG, "Removing " + numDeleted + " apks from repo " + repo.address);
-        }
-    }
-
-    private void removeAppsWithoutApks() {
-        int numDeleted = getContentResolver().delete(AppProvider.getNoApksUri(), null, null);
-        Utils.debugLog(TAG, "Removing " + numDeleted + " apks that don't have any apks");
-    }
-
     /**
      * Received progress event from the RepoXMLHandler. It could be progress
      * downloading from the repo, or perhaps processing the info from the repo.
@@ -783,6 +510,6 @@ public class UpdateService extends IntentService implements ProgressListener {
                 message = getString(R.string.status_processing_xml_percent, repoAddress, downloadedSize, totalSize, percent);
                 break;
         }
-        sendStatus(STATUS_INFO, message);
+        sendStatus(this, STATUS_INFO, message);
     }
 }

From 8d1e20b7fda804a1b64fa24b3a8bc390786b9c1a Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Sun, 13 Sep 2015 11:33:53 +1000
Subject: [PATCH 2/7] Remove TargetApi(8) because that is our min-sdk now.

---
 F-Droid/test/src/org/fdroid/fdroid/RepoUpdaterTest.java | 2 --
 1 file changed, 2 deletions(-)

diff --git a/F-Droid/test/src/org/fdroid/fdroid/RepoUpdaterTest.java b/F-Droid/test/src/org/fdroid/fdroid/RepoUpdaterTest.java
index 1c5a95999..a588df536 100644
--- a/F-Droid/test/src/org/fdroid/fdroid/RepoUpdaterTest.java
+++ b/F-Droid/test/src/org/fdroid/fdroid/RepoUpdaterTest.java
@@ -1,7 +1,6 @@
 
 package org.fdroid.fdroid;
 
-import android.annotation.TargetApi;
 import android.content.Context;
 import android.test.InstrumentationTestCase;
 
@@ -11,7 +10,6 @@ import org.fdroid.fdroid.data.Repo;
 import java.io.File;
 import java.util.UUID;
 
-@TargetApi(8)
 public class RepoUpdaterTest extends InstrumentationTestCase {
     private static final String TAG = "RepoUpdaterTest";
 

From f794d1e7a52809fb55679e0f76a0f481b9f32a3b Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Sun, 13 Sep 2015 11:52:07 +1000
Subject: [PATCH 3/7] Infrastructure for doing test driven development to
 support multiple repo dev.

The new test skeletons right now update three different repos
in different configurations. They do so such that the order of updates
changes and therefore the way in which conflicts between repos are
dealt with are tested.

They should all have the same result (though I'm not sure exactly what
that should be yet).
---
 F-Droid/test/assets/README.md                 |  50 +++++++
 F-Droid/test/assets/multiRepo.archive.jar     | Bin 0 -> 5971 bytes
 F-Droid/test/assets/multiRepo.conflicting.jar | Bin 0 -> 5705 bytes
 F-Droid/test/assets/multiRepo.normal.jar      | Bin 0 -> 5809 bytes
 .../fdroid/fdroid/MultiRepoUpdaterTest.java   | 136 ++++++++++++++++++
 5 files changed, 186 insertions(+)
 create mode 100644 F-Droid/test/assets/README.md
 create mode 100644 F-Droid/test/assets/multiRepo.archive.jar
 create mode 100644 F-Droid/test/assets/multiRepo.conflicting.jar
 create mode 100644 F-Droid/test/assets/multiRepo.normal.jar
 create mode 100644 F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java

diff --git a/F-Droid/test/assets/README.md b/F-Droid/test/assets/README.md
new file mode 100644
index 000000000..ea34d0a14
--- /dev/null
+++ b/F-Droid/test/assets/README.md
@@ -0,0 +1,50 @@
+# Multiple Repos Test
+
+This covers the three indexes:
+ * multiRepo.normal.jar
+ * multiRepo.archive.jar
+ * multiRepo.conflicting.jar
+
+The goal is that F-Droid client should be able to:
+
+ * Update all three repos successfully
+ * Show all included versions for download in the UI
+ * Somehow deal nicely with the fact that two repos provide versions 50-53 of AdAway
+
+## multiRepo.normal.jar
+
+ * 2048 (com.uberspot.a2048)
+   - Version 1.96 (19)
+   - Version 1.95 (18)
+ * AdAway (org.adaway)
+   - Version 3.0.2 (54)
+   - Version 3.0.1 (53)
+   - Version 3.0 (52)
+ * adbWireless (siir.es.adbWireless)
+   - Version 1.5.4 (12)
+
+## multiRepo.archive.jar
+
+ * AdAway (org.adaway)
+   - Version 2.9.2 (51)
+   - Version 2.9.1 (50)
+   - Version 2.9 (49)
+   - Version 2.8.1 (48)
+   - Version 2.7 (46)
+   - Version 2.6 (45)
+   - Version 2.3 (42)
+   - Version 2.1 (40)
+   - Version 1.37 (37)
+   - Version 1.35 (36)
+   - Version 1.34 (35)
+
+## multiRepo.conflicting.jar
+
+ * AdAway (org.adaway)
+   - Version 3.0.1 (53)
+   - Version 3.0 (52)
+   - Version 2.9.2 (51)
+   - Version 2.2.1 (50)
+ * Add to calendar (org.dgtale.icsimport)
+   - Version 1.2 (3)
+   - Version 1.1 (2)
\ No newline at end of file
diff --git a/F-Droid/test/assets/multiRepo.archive.jar b/F-Droid/test/assets/multiRepo.archive.jar
new file mode 100644
index 0000000000000000000000000000000000000000..f5c505c073234f2bed24b4c4888d61e1d395c601
GIT binary patch
literal 5971
zcma)AWmJ@1yB@kbhLRpqI;BTokOl{kMml8Zlnx02L1{!9Bm_jdhHi&$7?2d{4iPx`
zops*V?_KA7>s-&;&wBRS`@Wug-}}ej*QKS3hE5K^#>NJGu;Nhy`~mX&Z*@f-FpshZ
zgijr;p$t)k>hP*VPSn2+sj2etjuEKxa1DPQseZ~oDfDf_jaNnep|bjzXDK>P+75NM
zQ5u)sB@o2+y?{&nJCI@>2ucAYzc_X~a=QX;78!C{Z59^jZRxIW9=c)piPLO1Srwrn
zYM;8O3-gI`4q}U_xr@bdAXJXIxCG7z@l=FQx$7~0U;?yMv9OgU>5BvJvkSd%TK~WN
z6}6zsYML5myif=X0d@}Mr|LbD#97Ym<0%TZFt`7L)<8}Ez%Es>H7&356RtHUC@kQo
zfOPuqxpCHVcC@h@lB1m=$3a{9bwr9K%|qOXOT~4q7cJ{gGo9q0(AkwMOxqQ6(j?fm
zq%{^wTEui*Tc^UdIp1=!xZXO_kYN~ikwhXHwvC2wqjMq^j^t77o|aUqxFXpZ?nbl~
zGNk~KV&|F*4+cj#GR5{DRZSH5?fFjltbw#x*GUY^cw!_Y^3HnI$NA6eIENsM(B?$b
zxvC43tJIxq;O}~<5jj2?zOTjgf2;?^{per&`By!jLct%BY;`7ZD0>o#v7a)t;-Zc}
z=EJ!g)y)hhEeMwx$0}4!U1e5O;V2943_&MRl#?gH&Xs>m-9+pXnrv!~NleV76JnlT
z?e67$zUJ?LfxI5Rb{QdJ>~v_<;QUDSarZdPJmE3(E=Vq%LazP!L%MHktG5@JodY<i
zK6g84K+;E=XbKvp!Pw?NH@S9VY#j#=GL*x+KD15gXgf;`fH<(vlY@`cCR`Ms;QXvR
z4^SdYPFN5%v&X+2{EhJ|tkR`YOD*l0or0Ade=mzTaRTvXp92c;^Y%84KFsq|B7a*E
zX0Yzejr*?!?`Y!5J2>_xx+^i$M=-ktQ8+eAm;(r?`z{cN;z$|tvQF6mZ!w5_5!a5l
zCT%C$Z7eAyHk~CyiC{BH>H}H4F!wBck9t%kfWMAh{50zJa>?)A!{rZpDLY-t(lY@9
zs<~4ZUcu*(6cQ(QxqbTQ3&NizK<m?(rae5JvhRC;+NulWyeH%v^T)FASCs|JuQD)X
zAcVgWY==<m*|z0&-M;?Nxm#M^p;W&gM_b(8&#YMU@ToE@nvvmEZ`0$#4S2Cn6$Mpp
zx?MSCuFD$OCTL5&!<g3=@AL4ak!9@0skwgjO{AA+XBpt?G+Aq5v6TtgsoVxMA@QsC
zw@Ni-Sex_}X8}cWU8hy<>I+sDja8br9GAM33k0rM>+8pS8~@PL5~;MVWOya}D9$yB
z(CckElO9*slgDGO<j$%w(!m1BiK9d!wkg_7DK<d)45Bujmt>cb3jK=GpSWefYx(6<
zsiwx2QKxTa`^6y}!^yxIyTNp7!jBoYWNr`a$6}qmoAcKe8T3Y~33KK^aE5y3%UTGr
z^30VDT9`8qT)S679~9ZJ0Hs=-%oL_iZpBwG%5b3ClhSG2%&;^Dm5oW*4RPE^W<yP6
zQBCPhIEmX13H>D2O?fWnRvIghVU#oQH*c(D5=bkU6Kfw-Pse1u>6UPJd7ftrGr63I
zwIk8TyO@gWkr&=4Rd{<CB4C8F0tjp8D@0M2`&FR2>QYR68vB89(T!)r0)anp`Mv5<
zR*h5ba^qd)ce)@C_AI-~7W_}AO<&M4>I@&33|dW1!k07qgq^M{(=1j5Cnyj*fmL&6
zLk0N`vkPPIW>))Yv-#x-=w*^fZVx(~leoVd$$8L=;v5ZX)#RwKeHZ8PGt87up<27t
z5ro@!v-!GU2ANxNVO7B*Y$fVYG0{9~Rk|M-Zy30v8)XKNrOc#7rets&;>B?!4ow=u
zTXr*+XevR|L03qMW_MZgpse>=b+@4)H4ha}AJHC*ZI!CJ%uv!*iWl%l+%%e&TfJSp
zdCJ*<r_zN7JjY;|eb;9Hw%pqR9p!q$?-GOEA)-*>6u6~uV=A3cX$dpRb%9}gkG*!G
zU>pZ|tx4v-mUv}&<qi#KxmIiDv~~U@rp@ptHBeHmh2Emhpg89#TXqlGJD*71tS#2)
z&Nz0ty^%8rvH$Y9)C6!^>Wd4VD4)vCmTT)UNLzc{{zuEpwW3RM1%Diey34tC6sXoL
zFBmQs0vXek+_NYwU@tE{$ewRO$a_AxEgG_rXrH4f6mo-Xj;K4)HOPXiXwYRFni3@r
zwZNV&8Qq6$n%+INpB{wRn<HM`vK_fd3-OD4kN9}49?o<kTwR<rKUGqKoan~<oE~UO
zA7;&0Sm-b+mn=OPwNVmwi1wNg==HdDwL%|)33d<Avv4<Ye3dF)rqEzCX@55V)#=0{
zI>CsY@kk2He%Q4Pn#`uU;r6*xdB{P6DrkM0`C+Q3zA=0eKS^A27uMM8Ghp|Dy=;`v
zR|=H<bF2+JEWu&-)4PYCE&LZL4139VyUX9RS&VRy@+{KpPSajzIv=oMUOkY+FTjY~
z@*5!=E%f+lm3Jclfw^y_Q5I`Jl%xN3MsKk+CtPBF{#xx)CpVttnWsw)<n^9S_f@#t
zOyF_=o}gz?&ulN`Wt>Mi@=Wb@m03%M*-n0&6!DObj&3-!$SpPe#IJIfWpYziVS0>-
zw(cRBbo9+^Gpshm7f~=|6j9UlbGK#SLn$qq;uJy68_sz!D(kebj7c$iV?Oz8eE49r
zXKTqxx*3C};0|~Fiaz$2u5##XM?~Px0GFD35-HO%DVCx$;oWR;L!T)u*?d3ch3+*0
z`KI?W;z#t2x{(dviAi*nL27stG_}o|V8LWfrJjm6*{QtgsOiGz4#{}bY?$d3X}5i)
zP>`HD)j}aqfx2vJ2{@6g2l8e~a#RfI%Rsx-*!<0owt}w<d~-{ty+0Jz`M_($Mh;50
zebmnSZKp+6qFix@-Hi_{DHB%<5zuAAxmwRurK!=HfKOPSwv$SDXG0m}d-!S=sx>3p
zaz<(iPxzyS3J>zl9s5gHr@tkZ5{N?V-9_emK<Y&oz|t}e-?{Odl;BVHa8>5m-kk$*
zaYgv^xh8UMmG9CKqHUoIZU4HExO*7{%U}Tjb@%Q6zL4Mm(EmPwZwCPYw0{NPAIkvL
z->-Z8r(giU-pR_^ht~(@xM%pn-Kd`AhMO*6t44^h5kOg)Vx#1$-$ynxJgYL{O#i$p
zL3}z4#L2#2d4K|v+?E$98)i)ut=ky#<KqoRx}Wdv7jCxQ-LCo{YltmOO>L~P2o>fJ
z8~{(2M4y$_r8#>9h!_ksh}>pZ@M*@grEWZCVEC}*C3e+iYZe=lc++6U;Jog5rV->W
zY+yE1|MT|fZNAk26v+Vde%1dL6vm+HSW@T|=cy2fYHjrNThilraj{FIqlfe^9n@hg
zoRCKc(IqrWZ^K|;$0F$6p4)zUkQhX_7Ay4V^2yi={CSdLTsU_uL(GU1on`EGvtC$N
z+Gc-o2jO8u-#oQaOdDZeaQ=~XP^#;z^NaE2<(s1mN&goI7kSjCtYQ#thTFv;E~^VA
zd7bhfG%)pt;?Ic$eABvWQKa<WJr!cp3$gxW%q_-Vq{V^koS!s0C_nvHTJ`!&9<fd>
z<br7J%4!FfCZ)IyWjrQRHgtbeC6YQPsC=qh;HO?5O;#!Weh`VCyxNS1Dz9EgXMCXd
z<%~qn?^(W8^0XRuQKudSmgJKu*XY-<*=Yx+{ppXZjx0Z>bV$|VBua@KeZ2hD&vsL*
z3i785W8QY}LPrb(K5HxYNQxq`!a%VH(`-O@0T=UXd{V1KsCQ8}^LPvA)U+W~u%`yq
zWgvJXGgGWiNp8z;Fr767uOXRCuHuswo$)w6d)eZ9ujmRksv+GE)|~Cg2v)=VGeSHz
zl2lv~oI-sRp_jOv3PTyJ=le~x;@4^Fz>eY2q;~-cP9_8DFtP-KLoDus;d(WM0)o8C
zNdy8t#TA0?@LY$JG+u4u^)`~ClpvN^Ql#fr0wW(}ELrAC++JdPaRYba1kIrYWgo};
z-M+uZQByamA;$^9fJ6;YcYq##fJK36IiQ8_%F?=1ahI@W?Ve&0IBGbkaMI`YOuJh<
zCE?b`2CT2U|EQWC=VWuO>ZNydNS;(VNEUA<Mg97NPD%Kh5`_(o3p^^?tt$wjxrF&2
zqZ6wE_He^AKQl$@l+4bPX+_Ra*2GqLuxQmjB~T)A=vZsTB%<-Q`8JJ*^VQ{DEQpH!
z1&IQXQ;Y-a%+8`L<sFLjR59~A8dToWx=D+oB$A3MyYo4DT(`RUm=Jq}jyTa6n9ZQO
zyqwo9!p~Afu3Aq8e~D|>iB?}b7l`e3Sjs_>2`0}$vmyo&b4?sgIC<bGfkaVc@qf}f
z)>%q+bPP@lxtB6V(r^V2&MhU6x6PawfjVE}sGx(xz;9|^UvMFx<BX!=^Bo4B>ycvT
zMG*mZazMJ=XcT}S8I<Vh=fQR(8nvu=7dnm%rD13}dvo3sp}HjDU-0BUn~SxE%Z&2m
zVe4=_FB;EP#8ef0|IV-D*Q3)chh7Q(soK~ypw9y#_R=av=R{yJ77G<Evlv$R#q+#K
z$Bo=nM^^cWcJ#0u@3pN)^CVg!OlE~=1dVcv#b6$DoIENopYXJqDvt;*=1J1w5H~l1
zP|FYnH^UYQ$MX45;xNr%4xi`BISnEZt9TNd=bvc>-+Y=@nWA(fY*-OoueQ;MlbV!r
z#d{+tYNn5Rx>Oh&ZTV`p7W)FcCtk0|*c*&O3geL{$DL$gPVI2&_a{>bj~^$s_(D^N
zy^QBE73@h&sG2TL{t2Z>!8k0m=j{WLb`5l?ltV)~5?%tw!R^;CBh19XN+j*17*9o4
zJM|K%xNR;Ym}b!lzC7XPWn;w7dgJo3G)*4jA%9as^!T-4%eOCzE#l5YpU0=(;<i*`
zkVp4q=p^Piy%iPRAxlu#L4C)MV(634lH2idOrFD$n5g97ReBB{5|3O!5^T?c-&+)_
zb*h8w_~J>&=cX_wY=0ev+U^MS9+bhtLyk#K{cur_Qq<aBQ&++et4ZwOo-g&-)?d+J
zK&cTDbi9c6aq;8o0vh$0-Gn*FWSmroatB{=IiF8Ova2rRLnbteh+xL#Xt5{|rpheX
zuKYT$mNAnUh-1O25bGFTfXfHCkQ)^@N5>U>bQ=P|#KE8-wlI845%YKy9UaWo_t8qw
zlFO{SE9Ov1I%8K4$iXtyLsQ6J{YpMjSt)S`akZ9TjdW_g*>m*rt!?D^4!xo88usLR
z2CRO6>>*k@oG;l^CTaNfy_0pd;em_w5khaFzzoJVlUjzficGFcoZ495w0~swW5Y=z
zePmxoa9Oh{E%ofDKginn{ipf0{4&Iknwn!1-*^!%>N^*i?6}8`HAOHK-IHwX+pn`%
zyPJc*S{%J+cJ#cwotv+A_s_11i-CQ6KC;MLD|josf#dx}%*aUP`d)#sexniW4ApOG
z*p^{`SV=d~RtP$h59!P5J!=)BY?D~Nak?p+Kpyl)&fQKRo+iQ@o6;5=yv;f^o&7`9
zD;4<an|ywrop@VT)_-&tSJ12!<wo+Fwk_|Uu8zG(-a$A-!j>B^%ond0v>BW&x|Z!t
z@U2jG&zmli@yUmmo%^IDUV*BotS1gW?o^-chK2PBo$O7WK|GE^C_a`U=RA?sxv-60
zL-U>u5dHnS(ic`vkVXAQNV>~K$6BN*H{Gw5s>aOZA*ghlgtSM5`jyl9#_?s$Tq0k*
znUsl4&=vV+P^Q&V1@B7C-7H3ZJmT7SZch|X7@K(OYWLiOP_ShR31fY@m{epVbA25n
z8~e7b2=8DYf7UF$Qta7*9%Fl!=ADHt!}+l*-F#P@aiyqOfw5yng%^wezHvt7N{`#?
zM%mp?jT7>YBUflWgHxTA%u>UgmO&d5tBdGY;2MJb8Hq`K{idz8pXgkxSw0IgD_0F)
z32(t5VM-h2Rnll?Zgq%C-jciqI8R<fE<b|iYrd08e=#VZs#Byu7?<Py_i6!7s(J|%
zpZ7dpB0biVy`WSO`lfiB=wiI62yLpL&rEVatiqvd<7P_e_4S`^NL&L?egwYN50igR
zkH5_OCR@q?9(c3abG*KgS_yx7@^k&t>S}A@*@#Apb4rkm0sN)wFFK$ZTq3Z^|8%#f
z;j@&l$MxurGu#yZbMx$YD*xFK`;cvrA^hss#!#Y|rs?g`&iA3j4PZQ6`qtrU^J<9Q
z%)I>wcqh#CxwV?Cf1Ggd7k$i^M!BlmmmXDy&I|$Boe3rJVn?%BnRlRXn=<V`0PQ~}
zf>~&V1|JpZkPmUV7I44TmWy<Jqcg9~&vcd>Am3HkFKLH~#wB~|<bz*0Ess(^5Kg|v
z7kr2I?V=rVHR0ke!wScNxx^c5C*_sJtbqt{&2_e>YsTg-jOn;yxA^gIc(=d5o?m`7
z7C}Hxk6*83@5{fqe}}g3;q31&0XRIhZ;wfFbST{tCMfktj<9%iCWxy(z>=~+Zw6_M
z+peBfYh^vlN6M8@GvNC`!pKhk``;Z8h=L^pY4+2&46$mcF$qlfqyf6&=RWuIF9=$(
zJZAE=t2-XO=-pvYEd~iM*{xr$xIp>Cs@vKFfcriSiz|SJu8%8_Y=deRC}{HNWrPX$
zR9xMc+?%tc#0y7%{I#AMrBv8UL`bRfK*JIPJrV~|ypoNzxFuS&;kMkMvHZi@$RtZc
zHt77<RW(pENP#Q-y7(JWPN-}YE<`!+B&Dx;%_?RHvt2u^#@4OXV=ibx0In?9;c1H<
zn!rS@LPKxd$l|9j)8cmb3K#0lK(ULW^EtaHw9dw#amNsJYL(tQW2>!=&`wlQGg#&L
zC1?$M^0OqT_`4~>2ZN^B_cIszmfCH4ac4Z%MJs3tg@=w|wA1qdTAMX%WXW3iQ2C2K
zD~13{ZPs-0lZ*7eRNCl(C@^yZ!*XT__Kyx1KRe|bS&T*57&$Shx7n>Xmd?d~@U~3E
z?u#e`t<@{$9yWx%U6YR+$3^a`1^AUh<9qVBJ(JM71l-h|&5lud(**5XvD~|hsHN0i
zBnn20WKOWHgF!eg4S(1n>ktmOwD{P6%WlHtobVf^jR3g{B=|+belPHRTK6S?-Y&~_
z#GSmtO+XaAR(>;@!zpUH$Url322IGdVzc237CG(;vJ1x;SLH`_l#Mn_D@PRi-Dz43
zzan72C{^;ZS!wUOqmF+EJ2a!I?9t4IXX5va>XzuQC6x|dVx$*fSf}i03!?h|!Ugo`
zXMR@+t-wQpowdnL{!%#52~<$9^@huK&+S!C+z|Zs41-MjxMcAG02upk)q#Ra4*1&z
z{?lLm?E?Qh{_+t2@Pz-#@TVjB?+oqtWPdUI<w8Os|77@6QvG%=|HdfF|Hg3dWB!xk
zcgX(;tKZt|Z)oHELGfFO{WJ8>>fF!$18?rTf9WtSRSeAEV;J|(qx-bbDS!9=3pCZh
AC;$Ke

literal 0
HcmV?d00001

diff --git a/F-Droid/test/assets/multiRepo.conflicting.jar b/F-Droid/test/assets/multiRepo.conflicting.jar
new file mode 100644
index 0000000000000000000000000000000000000000..6d26f65d3159bf7af4ce3b0c5d45a729ce5d83d2
GIT binary patch
literal 5705
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

literal 0
HcmV?d00001

diff --git a/F-Droid/test/assets/multiRepo.normal.jar b/F-Droid/test/assets/multiRepo.normal.jar
new file mode 100644
index 0000000000000000000000000000000000000000..6a53256ebc4f7a670e7ee7af052814e3df29d1af
GIT binary patch
literal 5809
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
F{V&5kQ1$=-

literal 0
HcmV?d00001

diff --git a/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java b/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java
new file mode 100644
index 000000000..d3efcb624
--- /dev/null
+++ b/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java
@@ -0,0 +1,136 @@
+
+package org.fdroid.fdroid;
+
+import android.content.Context;
+import android.test.InstrumentationTestCase;
+
+import org.fdroid.fdroid.RepoUpdater.UpdateException;
+import org.fdroid.fdroid.data.Repo;
+
+import java.io.File;
+import java.util.UUID;
+
+public class MultiRepoUpdaterTest extends InstrumentationTestCase {
+    private static final String TAG = "RepoUpdaterTest";
+
+    private Context context;
+    private RepoUpdater conflictingRepoUpdater;
+    private RepoUpdater mainRepoUpdater;
+    private RepoUpdater archiveRepoUpdater;
+    private File testFilesDir;
+
+    private static final String PUB_KEY =
+            "3082050b308202f3a003020102020420d8f212300d06092a864886f70d01010b050030363110300e0603" +
+            "55040b1307462d44726f69643122302006035504031319657073696c6f6e2e70657465722e7365727779" +
+            "6c6f2e636f6d301e170d3135303931323233313632315a170d3433303132383233313632315a30363110" +
+            "300e060355040b1307462d44726f69643122302006035504031319657073696c6f6e2e70657465722e73" +
+            "657277796c6f2e636f6d30820222300d06092a864886f70d01010105000382020f003082020a02820201" +
+            "00b21fe72b84ce721967851364bd20511088d117bc3034e4bb4d3c1a06af2a308fdffdaf63b12e0926b9" +
+            "0545134b9ff570646cbcad89d9e86dcc8eb9977dd394240c75bccf5e8ddc3c5ef91b4f16eca5f36c36f1" +
+            "92463ff2c9257d3053b7c9ecdd1661bd01ec3fe70ee34a7e6b92ddba04f258a32d0cfb1b0ce85d047180" +
+            "97fc4bdfb54541b430dfcfc1c84458f9eb5627e0ec5341d561c3f15f228379a1282d241329198f31a7ac" +
+            "cd51ab2bbb881a1da55001123483512f77275f8990c872601198065b4e0137ddd1482e4fdefc73b857d4" +
+            "be324ca96c268ceb725398f8cc38a0dc6aa2c277f8686724e8c7ff3f320a05791fccacc6caa956cf23a9" +
+            "de2dc7070b262c0e35d90d17e90773bb11e875e79a8dfd958e359d5d5ad903a7cbc2955102502bd0134c" +
+            "a1ff7a0bbbbb57302e4a251e40724dcaa8ad024f4b3a71b8fceaac664c0dcc1995a1c4cf42676edad8bc" +
+            "b03ba255ab796677f18fff2298e1aaa5b134254b44d08a4d934c9859af7bbaf078c37b7f628db0e2cffb" +
+            "0493a669d5f4770d35d71284550ce06d6f6811cd2a31585085716257a4ba08ad968b0a2bf88f34ca2f2c" +
+            "73af1c042ab147597faccfb6516ef4468cfa0c5ab3c8120eaa7bac1080e4d2310f717db20815d0e1ee26" +
+            "bd4e47eed8d790892017ae9595365992efa1b7fd1bc1963f018264b2b3749b8f7b1907bb0843f1e7fc2d" +
+            "3f3b02284cd4bae0ab0203010001a321301f301d0603551d0e0416041456110e4fed863ab1df9448bfd9" +
+            "e10a8bc32ffe08300d06092a864886f70d01010b050003820201008082572ae930ebc55ecf1110f4bb72" +
+            "ad2a952c8ac6e65bd933706beb4a310e23deabb8ef6a7e93eea8217ab1f3f57b1f477f95f1d62eccb563" +
+            "67a4d70dfa6fcd2aace2bb00b90af39412a9441a9fae2396ff8b93de1df3d9837c599b1f80b7d75285cb" +
+            "df4539d7dd9612f54b45ca59bc3041c9b92fac12753fac154d12f31df360079ab69a2d20db9f6a7277a8" +
+            "259035e93de95e8cbc80351bc83dd24256183ea5e3e1db2a51ea314cdbc120c064b77e2eb3a731530511" +
+            "1e1dabed6996eb339b7cb948d05c1a84d63094b4a4c6d11389b2a7b5f2d7ecc9a149dda6c33705ef2249" +
+            "58afdfa1d98cf646dcf8857cd8342b1e07d62cb4313f35ad209046a4a42ff73f38cc740b1e695eeda49d" +
+            "5ea0384ad32f9e3ae54f6a48a558dbc7cccabd4e2b2286dc9c804c840bd02b9937841a0e48db00be9e3c" +
+            "d7120cf0f8648ce4ed63923f0352a2a7b3b97fc55ba67a7a218b8c0b3cda4a45861280a622e0a59cc9fb" +
+            "ca1117568126c581afa4408b0f5c50293c212c406b8ab8f50aad5ed0f038cfca580ef3aba7df25464d9e" +
+            "495ffb629922cfb511d45e6294c045041132452f1ed0f20ac3ab4792f610de1734e4c8b71d743c4b0101" +
+            "98f848e0dbfce5a0f2da0198c47e6935a47fda12c518ef45adfb66ddf5aebaab13948a66c004b8592d22" +
+            "e8af60597c4ae2977977cf61dc715a572e241ae717cafdb4f71781943945ac52e0f50b";
+
+    @Override
+    protected void setUp() {
+        context = getInstrumentation().getContext();
+        testFilesDir = TestUtils.getWriteableDir(getInstrumentation());
+        conflictingRepoUpdater = createUpdater(context);
+        mainRepoUpdater = createUpdater(context);
+        archiveRepoUpdater = createUpdater(context);
+    }
+
+    /**
+     * Check that a sample of expected apps and apk versions are available in the database.
+     * Also check that the AdAway apks versions 50-53 are as expected, given that 50 was in
+     * both conflicting and archive repo, and 51-53 were in both conflicting and main repo.
+     */
+    private void assertExpected() {
+
+    }
+
+    public void testConflictingThenMainThenArchive() throws UpdateException {
+        if (updateConflicting() && updateMain() && updateArchive()) {
+            assertExpected();
+        }
+    }
+
+    public void testConflictingThenArchiveThenMain() throws UpdateException {
+        if (updateConflicting() && updateArchive() && updateMain()) {
+            assertExpected();
+        }
+    }
+
+    public void testArchiveThenMainThenConflicting() throws UpdateException {
+        if (updateArchive() && updateMain() && updateConflicting()) {
+            assertExpected();
+        }
+    }
+
+    public void testArchiveThenConflictingThenMain() throws UpdateException {
+        if (updateArchive() && updateConflicting() && updateMain()) {
+            assertExpected();
+        }
+    }
+
+    public void testMainThenArchiveThenConflicting() throws UpdateException {
+        if (updateMain() && updateArchive() && updateConflicting()) {
+            assertExpected();
+        }
+    }
+
+    public void testMainThenConflictingThenArchive() throws UpdateException {
+        if (updateMain() && updateConflicting() && updateArchive()) {
+            assertExpected();
+        }
+    }
+
+    private RepoUpdater createUpdater(Context context) {
+        Repo repo = new Repo();
+        repo.pubkey = PUB_KEY;
+        return new RepoUpdater(context, repo);
+    }
+
+    private boolean updateConflicting() throws UpdateException {
+        return updateRepo(conflictingRepoUpdater, "multiRepo.conflicting.jar");
+    }
+
+    private boolean updateMain() throws UpdateException {
+        return updateRepo(mainRepoUpdater, "multiRepo.normal.jar");
+    }
+
+    private boolean updateArchive() throws UpdateException {
+        return updateRepo(archiveRepoUpdater, "multiRepo.archive.jar");
+    }
+
+    private boolean updateRepo(RepoUpdater updater, String indexJarPath) throws UpdateException {
+        if (!testFilesDir.canWrite())
+            return false;
+
+        File indexJar = TestUtils.copyAssetToDir(context, indexJarPath, testFilesDir);
+        updater.processDownloadedFile(indexJar, UUID.randomUUID().toString());
+        return true;
+    }
+
+}

From 0685c16efef3e9b8d9035681a2084e7a30fe6cc6 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Sat, 24 Oct 2015 09:00:27 +1100
Subject: [PATCH 4/7] More work on multi-repo tests, currently broken due to
 F-Droid being broken.

(One of) the problems with F-Droid's multiple support is that there is
a primary key on the fdroid_apk table which is a composite of:

 * id
 * vercode

Which means that two repos providing the same version means one will
update the other, rather than ending up with two different versions.

Instead, there should be some other way to differentiate apks from
different sources. Firstly, it should take into account the signing
cert. Secondly, it may taken into account the hash, because two people
could sign different apks with the same cert and then we are back at
square one.
---
 .../org/fdroid/fdroid/data/RepoProvider.java  |   4 +
 .../fdroid/FDroidTestWithAllProviders.java    |  23 ++
 .../fdroid/fdroid/MultiRepoUpdaterTest.java   | 239 +++++++++++++++++-
 .../test/src/org/fdroid/fdroid/TestUtils.java |  76 +++---
 4 files changed, 299 insertions(+), 43 deletions(-)
 create mode 100644 F-Droid/test/src/org/fdroid/fdroid/FDroidTestWithAllProviders.java

diff --git a/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java b/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java
index 5e27df4c2..62af6affd 100644
--- a/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java
+++ b/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java
@@ -244,6 +244,10 @@ public class RepoProvider extends FDroidProvider {
         matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, "#", CODE_SINGLE);
     }
 
+    public static String getAuthority() {
+        return AUTHORITY + "." + PROVIDER_NAME;
+    }
+
     public static Uri getContentUri() {
         return Uri.parse("content://" + AUTHORITY + "." + PROVIDER_NAME);
     }
diff --git a/F-Droid/test/src/org/fdroid/fdroid/FDroidTestWithAllProviders.java b/F-Droid/test/src/org/fdroid/fdroid/FDroidTestWithAllProviders.java
new file mode 100644
index 000000000..aad34777a
--- /dev/null
+++ b/F-Droid/test/src/org/fdroid/fdroid/FDroidTestWithAllProviders.java
@@ -0,0 +1,23 @@
+package org.fdroid.fdroid;
+
+import org.fdroid.fdroid.data.AppProvider;
+
+/**
+ * Class that makes available all ContentProviders that F-Droid owns.
+ */
+public abstract class FDroidTestWithAllProviders extends FDroidProviderTest<AppProvider> {
+
+    public FDroidTestWithAllProviders(Class<AppProvider> providerClass, String providerAuthority) {
+        super(providerClass, providerAuthority);
+    }
+
+    @Override
+    protected String[] getMinimalProjection() {
+        return new String[] {
+                AppProvider.DataColumns._ID,
+                AppProvider.DataColumns.APP_ID,
+                AppProvider.DataColumns.NAME,
+        };
+    }
+
+}
diff --git a/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java b/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java
index d3efcb624..0167a3031 100644
--- a/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java
+++ b/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java
@@ -1,23 +1,45 @@
 
 package org.fdroid.fdroid;
 
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
 import android.content.Context;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.support.annotation.NonNull;
 import android.test.InstrumentationTestCase;
+import android.test.RenamingDelegatingContext;
+import android.test.mock.MockContentResolver;
+import android.text.TextUtils;
+import android.util.Log;
 
 import org.fdroid.fdroid.RepoUpdater.UpdateException;
+import org.fdroid.fdroid.data.Apk;
+import org.fdroid.fdroid.data.ApkProvider;
+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 java.io.File;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.UUID;
 
 public class MultiRepoUpdaterTest extends InstrumentationTestCase {
     private static final String TAG = "RepoUpdaterTest";
 
+    private static final String REPO_MAIN = "Test F-Droid repo";
+    private static final String REPO_ARCHIVE = "Test F-Droid repo (Archive)";
+    private static final String REPO_CONFLICTING = "Test F-Droid repo with different apps";
+
     private Context context;
     private RepoUpdater conflictingRepoUpdater;
     private RepoUpdater mainRepoUpdater;
     private RepoUpdater archiveRepoUpdater;
     private File testFilesDir;
+    private RepoPersister persister;
 
     private static final String PUB_KEY =
             "3082050b308202f3a003020102020420d8f212300d06092a864886f70d01010b050030363110300e0603" +
@@ -52,13 +74,75 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase {
             "98f848e0dbfce5a0f2da0198c47e6935a47fda12c518ef45adfb66ddf5aebaab13948a66c004b8592d22" +
             "e8af60597c4ae2977977cf61dc715a572e241ae717cafdb4f71781943945ac52e0f50b";
 
+    public class TestContext extends RenamingDelegatingContext {
+
+        private MockContentResolver resolver;
+
+        public TestContext() {
+            super(getInstrumentation().getTargetContext(), "test.");
+
+            resolver = new MockContentResolver();
+            resolver.addProvider(AppProvider.getAuthority(), prepareProvider(new AppProvider()));
+            resolver.addProvider(ApkProvider.getAuthority(), prepareProvider(new ApkProvider()));
+            resolver.addProvider(RepoProvider.getAuthority(), prepareProvider(new RepoProvider()));
+        }
+
+        private ContentProvider prepareProvider(ContentProvider provider) {
+            provider.attachInfo(this, null);
+            provider.onCreate();
+            return provider;
+        }
+
+        @Override
+        public File getFilesDir() {
+            return getInstrumentation().getTargetContext().getFilesDir();
+        }
+
+        /**
+         * String resources used during testing (e.g. when bootstraping the database) are from
+         * the real org.fdroid.fdroid app, not the test org.fdroid.fdroid.test app.
+         */
+        @Override
+        public Resources getResources() {
+            return getInstrumentation().getTargetContext().getResources();
+        }
+
+        @Override
+        public ContentResolver getContentResolver() {
+            return resolver;
+        }
+
+        @Override
+        public AssetManager getAssets() {
+            return getInstrumentation().getContext().getAssets();
+        }
+
+        @Override
+        public File getDatabasePath(String name) {
+            return new File(getInstrumentation().getContext().getFilesDir(), "fdroid_test.db");
+        }
+    }
+
     @Override
-    protected void setUp() {
-        context = getInstrumentation().getContext();
+    public void setUp() throws Exception {
+        super.setUp();
+
+        context = new TestContext();
+
         testFilesDir = TestUtils.getWriteableDir(getInstrumentation());
-        conflictingRepoUpdater = createUpdater(context);
-        mainRepoUpdater = createUpdater(context);
-        archiveRepoUpdater = createUpdater(context);
+
+        // On a fresh database install, there will be F-Droid + GP repos, including their Archive
+        // repos that we are not interested in.
+        RepoProvider.Helper.remove(context, 1);
+        RepoProvider.Helper.remove(context, 2);
+        RepoProvider.Helper.remove(context, 3);
+        RepoProvider.Helper.remove(context, 4);
+
+        persister = new RepoPersister(context);
+
+        conflictingRepoUpdater = createUpdater(REPO_CONFLICTING, context);
+        mainRepoUpdater = createUpdater(REPO_MAIN, context);
+        archiveRepoUpdater = createUpdater(REPO_ARCHIVE, context);
     }
 
     /**
@@ -67,49 +151,189 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase {
      * both conflicting and archive repo, and 51-53 were in both conflicting and main repo.
      */
     private void assertExpected() {
+        Log.d(TAG, "Asserting all versions of each .apk are in index.");
 
+        persister.save(new ArrayList<Repo>(0));
+
+        List<Repo> repos = RepoProvider.Helper.all(context);
+        assertEquals("Repos", 3, repos.size());
+
+        assertMainRepo(repos);
+        assertMainArchiveRepo(repos);
+        assertConflictingRepo(repos);
+
+        String appId = "com.uberspot.a2048";
+        App app = AppProvider.Helper.findById(context.getContentResolver(), appId);
+        assertNotNull("App " + appId + " exists", app);
+    }
+
+    /**
+     *  + 2048 (com.uberspot.a2048)
+     *    - Version 1.96 (19)
+     *    - Version 1.95 (18)
+     *  + AdAway (org.adaway)
+     *    - Version 3.0.2 (54)
+     *    - Version 3.0.1 (53)
+     *    - Version 3.0 (52)
+     *  + adbWireless (siir.es.adbWireless)
+     *    - Version 1.5.4 (12)
+     */
+    private void assertMainRepo(List<Repo> allRepos) {
+        Repo repo = findRepo(REPO_MAIN, allRepos);
+
+        List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, ApkProvider.DataColumns.ALL);
+        assertEquals("Apks for main repo", apks.size(), 6);
+        assertApksExist(apks, "com.uberspot.a2048", new int[]{18, 19});
+        assertApksExist(apks, "org.adaway", new int[] { 52, 53, 54 });
+        assertApksExist(apks, "siir.es.adbWireless", new int[]{12});
+    }
+
+    /**
+     *  + AdAway (org.adaway)
+     *    - Version 2.9.2 (51)
+     *    - Version 2.9.1 (50)
+     *    - Version 2.9 (49)
+     *    - Version 2.8.1 (48)
+     *    - Version 2.8 (47)
+     *    - Version 2.7 (46)
+     *    - Version 2.6 (45)
+     *    - Version 2.3 (42)
+     *    - Version 2.1 (40)
+     *    - Version 1.37 (38)
+     *    - Version 1.36 (37)
+     *    - Version 1.35 (36)
+     *    - Version 1.34 (35)
+     */
+    private void assertMainArchiveRepo(List<Repo> allRepos) {
+        Repo repo = findRepo(REPO_ARCHIVE, allRepos);
+
+        List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, ApkProvider.DataColumns.ALL);
+        assertEquals("Apks for main archive repo", 13, apks.size());
+        assertApksExist(apks, "org.adaway", new int[] { 35, 36, 37, 38, 40, 42, 45, 46, 47, 48, 49, 50, 51 });
+    }
+
+    /**
+     * + AdAway (org.adaway)
+     *   - Version 3.0.1 (53) *
+     *   - Version 3.0 (52) *
+     *   - Version 2.9.2 (51) *
+     *   - Version 2.2.1 (50) *
+     * + Add to calendar (org.dgtale.icsimport)
+     *   - Version 1.2 (3)
+     *   - Version 1.1 (2)
+     */
+    private void assertConflictingRepo(List<Repo> allRepos) {
+        Repo repo = findRepo(REPO_CONFLICTING, allRepos);
+
+        List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, ApkProvider.DataColumns.ALL);
+        assertEquals("Apks for main repo", 6, apks.size());
+        assertApksExist(apks, "org.adaway", new int[]{50, 51, 52, 53});
+        assertApksExist(apks, "org.dgtale.icsimport", new int[]{ 2, 3 });
+    }
+
+    @NonNull
+    private Repo findRepo(@NonNull String name, List<Repo> allRepos) {
+        Repo repo = null;
+        for (Repo r : allRepos) {
+            if (TextUtils.equals(name, r.getName())) {
+                repo = r;
+                break;
+            }
+        }
+
+        assertNotNull("Repo " + allRepos, repo);
+        return repo;
+    }
+
+    /**
+     * Checks that each version of appId as specified in versionCodes is present in apksToCheck.
+     */
+    private void assertApksExist(List<Apk> apksToCheck, String appId, int[] versionCodes) {
+        for (int versionCode : versionCodes) {
+            boolean found = false;
+            for (Apk apk : apksToCheck) {
+                if (apk.vercode == versionCode && apk.id.equals(appId)) {
+                    found = true;
+                    break;
+                }
+            }
+
+            assertTrue("Found app " + appId + ", v" + versionCode, found);
+        }
+    }
+
+    private void assertEmpty() {
+        assertEquals("No apps present", 0, AppProvider.Helper.all(context.getContentResolver()).size());
+
+        String[] packages = {
+                "com.uberspot.a2048",
+                "org.adaway",
+                "siir.es.adbWireless",
+        };
+
+        for (String id : packages) {
+            assertEquals("No apks for " + id, 0, ApkProvider.Helper.findByApp(context, id).size());
+        }
     }
 
     public void testConflictingThenMainThenArchive() throws UpdateException {
+        assertEmpty();
         if (updateConflicting() && updateMain() && updateArchive()) {
             assertExpected();
         }
     }
 
     public void testConflictingThenArchiveThenMain() throws UpdateException {
+        assertEmpty();
         if (updateConflicting() && updateArchive() && updateMain()) {
             assertExpected();
         }
     }
 
     public void testArchiveThenMainThenConflicting() throws UpdateException {
+        assertEmpty();
         if (updateArchive() && updateMain() && updateConflicting()) {
             assertExpected();
         }
     }
 
     public void testArchiveThenConflictingThenMain() throws UpdateException {
+        assertEmpty();
         if (updateArchive() && updateConflicting() && updateMain()) {
             assertExpected();
         }
     }
 
     public void testMainThenArchiveThenConflicting() throws UpdateException {
+        assertEmpty();
         if (updateMain() && updateArchive() && updateConflicting()) {
             assertExpected();
         }
     }
 
     public void testMainThenConflictingThenArchive() throws UpdateException {
+        assertEmpty();
         if (updateMain() && updateConflicting() && updateArchive()) {
             assertExpected();
         }
     }
 
-    private RepoUpdater createUpdater(Context context) {
+    private RepoUpdater createUpdater(String name, Context context) {
         Repo repo = new Repo();
         repo.pubkey = PUB_KEY;
-        return new RepoUpdater(context, repo);
+        repo.address = UUID.randomUUID().toString();
+        repo.name = name;
+
+        ContentValues values = new ContentValues(2);
+        values.put(RepoProvider.DataColumns.PUBLIC_KEY, repo.pubkey);
+        values.put(RepoProvider.DataColumns.ADDRESS, repo.address);
+        values.put(RepoProvider.DataColumns.NAME, repo.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 new RepoUpdater(context, RepoProvider.Helper.findByAddress(context, repo.address));
     }
 
     private boolean updateConflicting() throws UpdateException {
@@ -130,6 +354,7 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase {
 
         File indexJar = TestUtils.copyAssetToDir(context, indexJarPath, testFilesDir);
         updater.processDownloadedFile(indexJar, UUID.randomUUID().toString());
+        persister.queueUpdater(updater);
         return true;
     }
 
diff --git a/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java b/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java
index 027c36188..21b673bda 100644
--- a/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java
+++ b/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java
@@ -7,6 +7,7 @@ import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Environment;
+import android.support.annotation.Nullable;
 import android.util.Log;
 
 import junit.framework.AssertionFailedError;
@@ -192,6 +193,7 @@ public class TestUtils {
 
     }
 
+    @Nullable
     public static File copyAssetToDir(Context context, String assetName, File directory) {
         File tempFile;
         InputStream input = null;
@@ -199,7 +201,7 @@ public class TestUtils {
         try {
             tempFile = File.createTempFile(assetName + "-", ".testasset", directory);
             Log.d(TAG, "Copying asset file " + assetName + " to directory " + directory);
-            input = context.getResources().getAssets().open(assetName);
+            input = context.getAssets().open(assetName);
             output = new FileOutputStream(tempFile);
             Utils.copy(input, output);
         } catch (IOException e) {
@@ -212,6 +214,18 @@ public class TestUtils {
         return tempFile;
     }
 
+    public static File getWriteableDir(Context context) {
+        File[] dirsToTry = new File[] {
+                context.getCacheDir(),
+                context.getFilesDir(),
+                context.getExternalCacheDir(),
+                context.getExternalFilesDir(null),
+                Environment.getExternalStorageDirectory()
+        };
+
+        return getWriteableDir(dirsToTry);
+    }
+    
     /**
      * Prefer internal over external storage, because external tends to be FAT filesystems,
      * which don't support symlinks (which we test using this method).
@@ -219,41 +233,31 @@ public class TestUtils {
     public static File getWriteableDir(Instrumentation instrumentation) {
         Context context = instrumentation.getContext();
         Context targetContext = instrumentation.getTargetContext();
-        File dir = context.getCacheDir();
-        Log.d(TAG, "Looking for writeable dir, trying context.getCacheDir()");
-        if (dir == null || !dir.canWrite()) {
-            Log.d(TAG, "Looking for writeable dir, trying context.getFilesDir()");
-            dir = context.getFilesDir();
+
+
+        File[] dirsToTry = new File[] {
+                context.getCacheDir(),
+                context.getFilesDir(),
+                targetContext.getCacheDir(),
+                targetContext.getFilesDir(),
+                context.getExternalCacheDir(),
+                context.getExternalFilesDir(null),
+                targetContext.getExternalCacheDir(),
+                targetContext.getExternalFilesDir(null),
+                Environment.getExternalStorageDirectory()
+        };
+
+        return getWriteableDir(dirsToTry);
+    }
+    
+    private static File getWriteableDir(File[] dirsToTry) {
+
+        for (File dir : dirsToTry) {
+            if (dir != null && dir.canWrite()) {
+                return dir;
+            }
         }
-        if (dir == null || !dir.canWrite()) {
-            Log.d(TAG, "Looking for writeable dir, trying targetContext.getCacheDir()");
-            dir = targetContext.getCacheDir();
-        }
-        if (dir == null || !dir.canWrite()) {
-            Log.d(TAG, "Looking for writeable dir, trying targetContext.getFilesDir()");
-            dir = targetContext.getFilesDir();
-        }
-        if (dir == null || !dir.canWrite()) {
-            Log.d(TAG, "Looking for writeable dir, trying context.getExternalCacheDir()");
-            dir = context.getExternalCacheDir();
-        }
-        if (dir == null || !dir.canWrite()) {
-            Log.d(TAG, "Looking for writeable dir, trying context.getExternalFilesDir(null)");
-            dir = context.getExternalFilesDir(null);
-        }
-        if (dir == null || !dir.canWrite()) {
-            Log.d(TAG, "Looking for writeable dir, trying targetContext.getExternalCacheDir()");
-            dir = targetContext.getExternalCacheDir();
-        }
-        if (dir == null || !dir.canWrite()) {
-            Log.d(TAG, "Looking for writeable dir, trying targetContext.getExternalFilesDir(null)");
-            dir = targetContext.getExternalFilesDir(null);
-        }
-        if (dir == null || !dir.canWrite()) {
-            Log.d(TAG, "Looking for writeable dir, trying Environment.getExternalStorageDirectory()");
-            dir = Environment.getExternalStorageDirectory();
-        }
-        Log.d(TAG, "Writeable dir found: " + dir);
-        return dir;
+
+        return null;
     }
 }

From 1c179848b7510cb11e3582c427624d0390223db1 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Wed, 4 Nov 2015 21:39:10 +1100
Subject: [PATCH 5/7] Added tests for current multi-repo behaviour.

This is the bare minimum of what must be maintained going forward.
Ideally the behaviour sohould be better, but that is for the future.
---
 .../fdroid/fdroid/MultiRepoUpdaterTest.java   | 120 +++++++++++++++---
 1 file changed, 103 insertions(+), 17 deletions(-)

diff --git a/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java b/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java
index 0167a3031..9a02c5818 100644
--- a/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java
+++ b/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java
@@ -146,25 +146,53 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase {
     }
 
     /**
-     * Check that a sample of expected apps and apk versions are available in the database.
-     * Also check that the AdAway apks versions 50-53 are as expected, given that 50 was in
-     * both conflicting and archive repo, and 51-53 were in both conflicting and main repo.
+     * Check that all of the expected apps and apk versions are available in the database. This
+     * check will take into account the repository the apks came from, to ensure that each
+     * repository indeed contains the apks that it said it would provide.
      */
     private void assertExpected() {
         Log.d(TAG, "Asserting all versions of each .apk are in index.");
-
-        persister.save(new ArrayList<Repo>(0));
-
         List<Repo> repos = RepoProvider.Helper.all(context);
         assertEquals("Repos", 3, repos.size());
 
         assertMainRepo(repos);
         assertMainArchiveRepo(repos);
         assertConflictingRepo(repos);
+    }
 
-        String appId = "com.uberspot.a2048";
-        App app = AppProvider.Helper.findById(context.getContentResolver(), appId);
-        assertNotNull("App " + appId + " exists", app);
+    /**
+     *
+     */
+    private void assertSomewhatAcceptable() {
+        Log.d(TAG, "Asserting at least one versions of each .apk is in index.");
+        List<Repo> repos = RepoProvider.Helper.all(context);
+        assertEquals("Repos", 3, repos.size());
+
+        assertApp2048();
+        assertAppAdaway();
+        assertAppAdbWireless();
+        assertAppIcsImport();
+    }
+
+    private void assertApp(String packageName, int[] versionCodes) {
+        List<Apk> apks = ApkProvider.Helper.findByApp(context, packageName, ApkProvider.DataColumns.ALL);
+        assertApksExist(apks, packageName, versionCodes);
+    }
+
+    private void assertApp2048() {
+        assertApp("com.uberspot.a2048", new int[]{ 19, 18 });
+    }
+
+    private void assertAppAdaway() {
+        assertApp("org.adaway", new int[]{ 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 42, 40, 38, 37, 36, 35 });
+    }
+
+    private void assertAppAdbWireless() {
+        assertApp("siir.es.adbWireless", new int[]{ 12 });
+    }
+
+    private void assertAppIcsImport() {
+        assertApp("org.dgtale.icsimport", new int[] { 3, 2 });
     }
 
     /**
@@ -184,7 +212,7 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase {
         List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, ApkProvider.DataColumns.ALL);
         assertEquals("Apks for main repo", apks.size(), 6);
         assertApksExist(apks, "com.uberspot.a2048", new int[]{18, 19});
-        assertApksExist(apks, "org.adaway", new int[] { 52, 53, 54 });
+        assertApksExist(apks, "org.adaway", new int[]{52, 53, 54});
         assertApksExist(apks, "siir.es.adbWireless", new int[]{12});
     }
 
@@ -228,7 +256,7 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase {
         List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, ApkProvider.DataColumns.ALL);
         assertEquals("Apks for main repo", 6, apks.size());
         assertApksExist(apks, "org.adaway", new int[]{50, 51, 52, 53});
-        assertApksExist(apks, "org.dgtale.icsimport", new int[]{ 2, 3 });
+        assertApksExist(apks, "org.dgtale.icsimport", new int[]{2, 3});
     }
 
     @NonNull
@@ -276,48 +304,106 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase {
         }
     }
 
-    public void testConflictingThenMainThenArchive() throws UpdateException {
+    private void persistData() {
+        persister.save(new ArrayList<Repo>(0));
+    }
+
+    public void testCorrectConflictingThenMainThenArchive() throws UpdateException {
         assertEmpty();
         if (updateConflicting() && updateMain() && updateArchive()) {
+            persistData();
             assertExpected();
         }
     }
 
-    public void testConflictingThenArchiveThenMain() throws UpdateException {
+    public void testCorrectConflictingThenArchiveThenMain() throws UpdateException {
         assertEmpty();
         if (updateConflicting() && updateArchive() && updateMain()) {
+            persistData();
             assertExpected();
         }
     }
 
-    public void testArchiveThenMainThenConflicting() throws UpdateException {
+    public void testCorrectArchiveThenMainThenConflicting() throws UpdateException {
         assertEmpty();
         if (updateArchive() && updateMain() && updateConflicting()) {
+            persistData();
             assertExpected();
         }
     }
 
-    public void testArchiveThenConflictingThenMain() throws UpdateException {
+    public void testCorrectArchiveThenConflictingThenMain() throws UpdateException {
         assertEmpty();
         if (updateArchive() && updateConflicting() && updateMain()) {
+            persistData();
             assertExpected();
         }
     }
 
-    public void testMainThenArchiveThenConflicting() throws UpdateException {
+    public void testCorrectMainThenArchiveThenConflicting() throws UpdateException {
         assertEmpty();
         if (updateMain() && updateArchive() && updateConflicting()) {
+            persistData();
             assertExpected();
         }
     }
 
-    public void testMainThenConflictingThenArchive() throws UpdateException {
+    public void testCorrectMainThenConflictingThenArchive() throws UpdateException {
         assertEmpty();
         if (updateMain() && updateConflicting() && updateArchive()) {
+            persistData();
             assertExpected();
         }
     }
 
+    public void testAcceptableConflictingThenMainThenArchive() throws UpdateException {
+        assertEmpty();
+        if (updateConflicting() && updateMain() && updateArchive()) {
+            persistData();
+            assertSomewhatAcceptable();
+        }
+    }
+
+    public void testAcceptableConflictingThenArchiveThenMain() throws UpdateException {
+        assertEmpty();
+        if (updateConflicting() && updateArchive() && updateMain()) {
+            persistData();
+            assertSomewhatAcceptable();
+        }
+    }
+
+    public void testAcceptableArchiveThenMainThenConflicting() throws UpdateException {
+        assertEmpty();
+        if (updateArchive() && updateMain() && updateConflicting()) {
+            persistData();
+            assertSomewhatAcceptable();
+        }
+    }
+
+    public void testAcceptableArchiveThenConflictingThenMain() throws UpdateException {
+        assertEmpty();
+        if (updateArchive() && updateConflicting() && updateMain()) {
+            persistData();
+            assertSomewhatAcceptable();
+        }
+    }
+
+    public void testAcceptableMainThenArchiveThenConflicting() throws UpdateException {
+        assertEmpty();
+        if (updateMain() && updateArchive() && updateConflicting()) {
+            persistData();
+            assertSomewhatAcceptable();
+        }
+    }
+
+    public void testAcceptableMainThenConflictingThenArchive() throws UpdateException {
+        assertEmpty();
+        if (updateMain() && updateConflicting() && updateArchive()) {
+            persistData();
+            assertSomewhatAcceptable();
+        }
+    }
+
     private RepoUpdater createUpdater(String name, Context context) {
         Repo repo = new Repo();
         repo.pubkey = PUB_KEY;

From 938c992023bb95ae1cae6e0677717e78e1a84a45 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Wed, 4 Nov 2015 21:43:34 +1100
Subject: [PATCH 6/7] Comment out tests for future, desirable behaviour.

Leave only the ones which align with the current multi-repo behaviour
that F-Droid exhibits.

The commented out tests can be uncommented in the future when working
on proper multi-repo support.
---
 .../test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java    | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java b/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java
index 9a02c5818..19c187d68 100644
--- a/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java
+++ b/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java
@@ -308,6 +308,10 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase {
         persister.save(new ArrayList<Repo>(0));
     }
 
+	/* At time fo writing, the following tests did not pass. This is because the multi-repo support
+       in F-Droid was not sufficient. When working on proper multi repo support than this should be
+       ucommented and all these tests should pass:
+
     public void testCorrectConflictingThenMainThenArchive() throws UpdateException {
         assertEmpty();
         if (updateConflicting() && updateMain() && updateArchive()) {
@@ -356,6 +360,8 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase {
         }
     }
 
+	*/
+
     public void testAcceptableConflictingThenMainThenArchive() throws UpdateException {
         assertEmpty();
         if (updateConflicting() && updateMain() && updateArchive()) {

From 12d5c5c7b49bba5bf884f5a6c5d909104c3b5b80 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Sat, 7 Nov 2015 09:40:44 +1100
Subject: [PATCH 7/7] Format to make checkstyle happy. Remove unused code.

---
 .../src/org/fdroid/fdroid/RepoPersister.java  | 24 +++---
 .../fdroid/FDroidTestWithAllProviders.java    | 23 ------
 .../fdroid/fdroid/MultiRepoUpdaterTest.java   | 81 +++++++++----------
 .../test/src/org/fdroid/fdroid/TestUtils.java | 58 ++++++-------
 4 files changed, 75 insertions(+), 111 deletions(-)
 delete mode 100644 F-Droid/test/src/org/fdroid/fdroid/FDroidTestWithAllProviders.java

diff --git a/F-Droid/src/org/fdroid/fdroid/RepoPersister.java b/F-Droid/src/org/fdroid/fdroid/RepoPersister.java
index 3f82422be..57aba7d34 100644
--- a/F-Droid/src/org/fdroid/fdroid/RepoPersister.java
+++ b/F-Droid/src/org/fdroid/fdroid/RepoPersister.java
@@ -34,13 +34,13 @@ public class RepoPersister {
      * values changed in the index, some fields should not be updated. Rather, they should be
      * ignored, because they were explicitly set by the user, and hence can't be automatically
      * overridden by the index.
-     *
+     * <p/>
      * NOTE: In the future, these attributes will be moved to a join table, so that the app table
      * is essentially completely transient, and can be nuked at any time.
      */
     private static final String[] APP_FIELDS_TO_IGNORE = {
-            AppProvider.DataColumns.IGNORE_ALLUPDATES,
-            AppProvider.DataColumns.IGNORE_THISUPDATE,
+        AppProvider.DataColumns.IGNORE_ALLUPDATES,
+        AppProvider.DataColumns.IGNORE_THISUPDATE,
     };
 
     @NonNull
@@ -129,9 +129,9 @@ public class RepoPersister {
         List<Apk> toRemove = new ArrayList<>();
 
         final String[] fields = {
-                ApkProvider.DataColumns.APK_ID,
-                ApkProvider.DataColumns.VERSION_CODE,
-                ApkProvider.DataColumns.VERSION,
+            ApkProvider.DataColumns.APK_ID,
+            ApkProvider.DataColumns.VERSION_CODE,
+            ApkProvider.DataColumns.VERSION,
         };
 
         for (final Repo repo : updatedRepos) {
@@ -175,14 +175,14 @@ public class RepoPersister {
                                         List<ContentProviderOperation> operations,
                                         int currentCount,
                                         int totalUpdateCount)
-            throws RemoteException, OperationApplicationException {
+        throws RemoteException, OperationApplicationException {
         int i = 0;
         while (i < operations.size()) {
             int count = Math.min(operations.size() - i, 100);
             ArrayList<ContentProviderOperation> o = new ArrayList<>(operations.subList(i, i + count));
             UpdateService.sendStatus(context, UpdateService.STATUS_INFO, context.getString(
-                    R.string.status_inserting,
-                    (int) ((double) (currentCount + i) / totalUpdateCount * 100)));
+                R.string.status_inserting,
+                (int) ((double) (currentCount + i) / totalUpdateCount * 100)));
             context.getContentResolver().applyBatch(providerAuthority, o);
             i += 100;
         }
@@ -193,9 +193,9 @@ public class RepoPersister {
      */
     private List<Apk> getKnownApks(List<Apk> apks) {
         final String[] fields = {
-                ApkProvider.DataColumns.APK_ID,
-                ApkProvider.DataColumns.VERSION,
-                ApkProvider.DataColumns.VERSION_CODE,
+            ApkProvider.DataColumns.APK_ID,
+            ApkProvider.DataColumns.VERSION,
+            ApkProvider.DataColumns.VERSION_CODE,
         };
         return ApkProvider.Helper.knownApks(context, apks, fields);
     }
diff --git a/F-Droid/test/src/org/fdroid/fdroid/FDroidTestWithAllProviders.java b/F-Droid/test/src/org/fdroid/fdroid/FDroidTestWithAllProviders.java
deleted file mode 100644
index aad34777a..000000000
--- a/F-Droid/test/src/org/fdroid/fdroid/FDroidTestWithAllProviders.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.fdroid.fdroid;
-
-import org.fdroid.fdroid.data.AppProvider;
-
-/**
- * Class that makes available all ContentProviders that F-Droid owns.
- */
-public abstract class FDroidTestWithAllProviders extends FDroidProviderTest<AppProvider> {
-
-    public FDroidTestWithAllProviders(Class<AppProvider> providerClass, String providerAuthority) {
-        super(providerClass, providerAuthority);
-    }
-
-    @Override
-    protected String[] getMinimalProjection() {
-        return new String[] {
-                AppProvider.DataColumns._ID,
-                AppProvider.DataColumns.APP_ID,
-                AppProvider.DataColumns.NAME,
-        };
-    }
-
-}
diff --git a/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java b/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java
index 19c187d68..71c6e43ff 100644
--- a/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java
+++ b/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java
@@ -17,7 +17,6 @@ import android.util.Log;
 import org.fdroid.fdroid.RepoUpdater.UpdateException;
 import org.fdroid.fdroid.data.Apk;
 import org.fdroid.fdroid.data.ApkProvider;
-import org.fdroid.fdroid.data.App;
 import org.fdroid.fdroid.data.AppProvider;
 import org.fdroid.fdroid.data.Repo;
 import org.fdroid.fdroid.data.RepoProvider;
@@ -42,7 +41,7 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase {
     private RepoPersister persister;
 
     private static final String PUB_KEY =
-            "3082050b308202f3a003020102020420d8f212300d06092a864886f70d01010b050030363110300e0603" +
+        "3082050b308202f3a003020102020420d8f212300d06092a864886f70d01010b050030363110300e0603" +
             "55040b1307462d44726f69643122302006035504031319657073696c6f6e2e70657465722e7365727779" +
             "6c6f2e636f6d301e170d3135303931323233313632315a170d3433303132383233313632315a30363110" +
             "300e060355040b1307462d44726f69643122302006035504031319657073696c6f6e2e70657465722e73" +
@@ -180,31 +179,31 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase {
     }
 
     private void assertApp2048() {
-        assertApp("com.uberspot.a2048", new int[]{ 19, 18 });
+        assertApp("com.uberspot.a2048", new int[]{19, 18});
     }
 
     private void assertAppAdaway() {
-        assertApp("org.adaway", new int[]{ 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 42, 40, 38, 37, 36, 35 });
+        assertApp("org.adaway", new int[]{54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 42, 40, 38, 37, 36, 35});
     }
 
     private void assertAppAdbWireless() {
-        assertApp("siir.es.adbWireless", new int[]{ 12 });
+        assertApp("siir.es.adbWireless", new int[]{12});
     }
 
     private void assertAppIcsImport() {
-        assertApp("org.dgtale.icsimport", new int[] { 3, 2 });
+        assertApp("org.dgtale.icsimport", new int[]{3, 2});
     }
 
     /**
-     *  + 2048 (com.uberspot.a2048)
-     *    - Version 1.96 (19)
-     *    - Version 1.95 (18)
-     *  + AdAway (org.adaway)
-     *    - Version 3.0.2 (54)
-     *    - Version 3.0.1 (53)
-     *    - Version 3.0 (52)
-     *  + adbWireless (siir.es.adbWireless)
-     *    - Version 1.5.4 (12)
+     * + 2048 (com.uberspot.a2048)
+     * - Version 1.96 (19)
+     * - Version 1.95 (18)
+     * + AdAway (org.adaway)
+     * - Version 3.0.2 (54)
+     * - Version 3.0.1 (53)
+     * - Version 3.0 (52)
+     * + adbWireless (siir.es.adbWireless)
+     * - Version 1.5.4 (12)
      */
     private void assertMainRepo(List<Repo> allRepos) {
         Repo repo = findRepo(REPO_MAIN, allRepos);
@@ -217,38 +216,38 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase {
     }
 
     /**
-     *  + AdAway (org.adaway)
-     *    - Version 2.9.2 (51)
-     *    - Version 2.9.1 (50)
-     *    - Version 2.9 (49)
-     *    - Version 2.8.1 (48)
-     *    - Version 2.8 (47)
-     *    - Version 2.7 (46)
-     *    - Version 2.6 (45)
-     *    - Version 2.3 (42)
-     *    - Version 2.1 (40)
-     *    - Version 1.37 (38)
-     *    - Version 1.36 (37)
-     *    - Version 1.35 (36)
-     *    - Version 1.34 (35)
+     * + AdAway (org.adaway)
+     * - Version 2.9.2 (51)
+     * - Version 2.9.1 (50)
+     * - Version 2.9 (49)
+     * - Version 2.8.1 (48)
+     * - Version 2.8 (47)
+     * - Version 2.7 (46)
+     * - Version 2.6 (45)
+     * - Version 2.3 (42)
+     * - Version 2.1 (40)
+     * - Version 1.37 (38)
+     * - Version 1.36 (37)
+     * - Version 1.35 (36)
+     * - Version 1.34 (35)
      */
     private void assertMainArchiveRepo(List<Repo> allRepos) {
         Repo repo = findRepo(REPO_ARCHIVE, allRepos);
 
         List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, ApkProvider.DataColumns.ALL);
         assertEquals("Apks for main archive repo", 13, apks.size());
-        assertApksExist(apks, "org.adaway", new int[] { 35, 36, 37, 38, 40, 42, 45, 46, 47, 48, 49, 50, 51 });
+        assertApksExist(apks, "org.adaway", new int[]{35, 36, 37, 38, 40, 42, 45, 46, 47, 48, 49, 50, 51});
     }
 
     /**
      * + AdAway (org.adaway)
-     *   - Version 3.0.1 (53) *
-     *   - Version 3.0 (52) *
-     *   - Version 2.9.2 (51) *
-     *   - Version 2.2.1 (50) *
+     * - Version 3.0.1 (53) *
+     * - Version 3.0 (52) *
+     * - Version 2.9.2 (51) *
+     * - Version 2.2.1 (50) *
      * + Add to calendar (org.dgtale.icsimport)
-     *   - Version 1.2 (3)
-     *   - Version 1.1 (2)
+     * - Version 1.2 (3)
+     * - Version 1.1 (2)
      */
     private void assertConflictingRepo(List<Repo> allRepos) {
         Repo repo = findRepo(REPO_CONFLICTING, allRepos);
@@ -294,9 +293,9 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase {
         assertEquals("No apps present", 0, AppProvider.Helper.all(context.getContentResolver()).size());
 
         String[] packages = {
-                "com.uberspot.a2048",
-                "org.adaway",
-                "siir.es.adbWireless",
+            "com.uberspot.a2048",
+            "org.adaway",
+            "siir.es.adbWireless",
         };
 
         for (String id : packages) {
@@ -308,7 +307,7 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase {
         persister.save(new ArrayList<Repo>(0));
     }
 
-	/* At time fo writing, the following tests did not pass. This is because the multi-repo support
+    /* At time fo writing, the following tests did not pass. This is because the multi-repo support
        in F-Droid was not sufficient. When working on proper multi repo support than this should be
        ucommented and all these tests should pass:
 
@@ -360,7 +359,7 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase {
         }
     }
 
-	*/
+    */
 
     public void testAcceptableConflictingThenMainThenArchive() throws UpdateException {
         assertEmpty();
diff --git a/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java b/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java
index 21b673bda..0869383de 100644
--- a/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java
+++ b/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java
@@ -68,10 +68,10 @@ public class TestUtils {
         if (actualList.size() != expectedContains.size()) {
             String message =
                 "List sizes don't match.\n" +
-                "Expected: " +
-                listToString(expectedContains) + "\n" +
-                "Actual:   " +
-                listToString(actualList);
+                    "Expected: " +
+                    listToString(expectedContains) + "\n" +
+                    "Actual:   " +
+                    listToString(actualList);
             throw new AssertionFailedError(message);
         }
         for (T required : expectedContains) {
@@ -85,10 +85,10 @@ public class TestUtils {
             if (!containsRequired) {
                 String message =
                     "List doesn't contain \"" + required + "\".\n" +
-                    "Expected: " +
-                    listToString(expectedContains) + "\n" +
-                    "Actual:   " +
-                    listToString(actualList);
+                        "Expected: " +
+                        listToString(expectedContains) + "\n" +
+                        "Actual:   " +
+                        listToString(actualList);
                 throw new AssertionFailedError(message);
             }
         }
@@ -151,8 +151,8 @@ public class TestUtils {
      * "installed apps" table in the database.
      */
     public static void installAndBroadcast(
-            MockContextSwappableComponents context,  MockInstallablePackageManager pm,
-            String appId, int versionCode, String versionName) {
+        MockContextSwappableComponents context, MockInstallablePackageManager pm,
+        String appId, int versionCode, String versionName) {
 
         context.setPackageManager(pm);
         pm.install(appId, versionCode, versionName);
@@ -166,8 +166,8 @@ public class TestUtils {
      * @see org.fdroid.fdroid.TestUtils#installAndBroadcast(mock.MockContextSwappableComponents, mock.MockInstallablePackageManager, String, int, String)
      */
     public static void upgradeAndBroadcast(
-            MockContextSwappableComponents context, MockInstallablePackageManager pm,
-            String appId, int versionCode, String versionName) {
+        MockContextSwappableComponents context, MockInstallablePackageManager pm,
+        String appId, int versionCode, String versionName) {
         /*
         removeAndBroadcast(context, pm, appId);
         installAndBroadcast(context, pm, appId, versionCode, versionName);
@@ -214,18 +214,6 @@ public class TestUtils {
         return tempFile;
     }
 
-    public static File getWriteableDir(Context context) {
-        File[] dirsToTry = new File[] {
-                context.getCacheDir(),
-                context.getFilesDir(),
-                context.getExternalCacheDir(),
-                context.getExternalFilesDir(null),
-                Environment.getExternalStorageDirectory()
-        };
-
-        return getWriteableDir(dirsToTry);
-    }
-    
     /**
      * Prefer internal over external storage, because external tends to be FAT filesystems,
      * which don't support symlinks (which we test using this method).
@@ -235,21 +223,21 @@ public class TestUtils {
         Context targetContext = instrumentation.getTargetContext();
 
 
-        File[] dirsToTry = new File[] {
-                context.getCacheDir(),
-                context.getFilesDir(),
-                targetContext.getCacheDir(),
-                targetContext.getFilesDir(),
-                context.getExternalCacheDir(),
-                context.getExternalFilesDir(null),
-                targetContext.getExternalCacheDir(),
-                targetContext.getExternalFilesDir(null),
-                Environment.getExternalStorageDirectory()
+        File[] dirsToTry = new File[]{
+            context.getCacheDir(),
+            context.getFilesDir(),
+            targetContext.getCacheDir(),
+            targetContext.getFilesDir(),
+            context.getExternalCacheDir(),
+            context.getExternalFilesDir(null),
+            targetContext.getExternalCacheDir(),
+            targetContext.getExternalFilesDir(null),
+            Environment.getExternalStorageDirectory(),
         };
 
         return getWriteableDir(dirsToTry);
     }
-    
+
     private static File getWriteableDir(File[] dirsToTry) {
 
         for (File dir : dirsToTry) {