diff --git a/app/src/androidTest/java/org/fdroid/fdroid/updater/SwapRepoEmulatorTest.java b/app/src/androidTest/java/org/fdroid/fdroid/updater/SwapRepoEmulatorTest.java
new file mode 100644
index 000000000..390763942
--- /dev/null
+++ b/app/src/androidTest/java/org/fdroid/fdroid/updater/SwapRepoEmulatorTest.java
@@ -0,0 +1,199 @@
+package org.fdroid.fdroid.updater;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ResolveInfo;
+import android.os.Looper;
+import android.support.test.InstrumentationRegistry;
+import android.text.TextUtils;
+import android.util.Log;
+import org.fdroid.fdroid.BuildConfig;
+import org.fdroid.fdroid.FDroidApp;
+import org.fdroid.fdroid.Hasher;
+import org.fdroid.fdroid.IndexUpdater;
+import org.fdroid.fdroid.Preferences;
+import org.fdroid.fdroid.Utils;
+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 org.fdroid.fdroid.data.Schema;
+import org.fdroid.fdroid.localrepo.LocalRepoKeyStore;
+import org.fdroid.fdroid.localrepo.LocalRepoManager;
+import org.fdroid.fdroid.localrepo.LocalRepoService;
+import org.fdroid.fdroid.net.LocalHTTPD;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.Socket;
+import java.security.cert.Certificate;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class SwapRepoEmulatorTest {
+    public static final String TAG = "SwapRepoEmulatorTest";
+
+    /**
+     * @see org.fdroid.fdroid.net.WifiStateChangeService.WifiInfoThread#run()
+     */
+    @Test
+    public void testSwap()
+            throws IOException, LocalRepoKeyStore.InitException, IndexUpdater.UpdateException, InterruptedException {
+        Looper.prepare();
+        LocalHTTPD localHttpd = null;
+        try {
+            Log.i(TAG, "REPO: " + FDroidApp.repo);
+            final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+            Preferences.setupForTests(context);
+
+            FDroidApp.initWifiSettings();
+            assertNull(FDroidApp.repo.address);
+
+            final CountDownLatch latch = new CountDownLatch(1);
+            new Thread() {
+                @Override
+                public void run() {
+                    while (FDroidApp.repo.address == null) {
+                        try {
+                            Log.i(TAG, "Waiting for IP address... " + FDroidApp.repo.address);
+                            Thread.sleep(1000);
+                        } catch (InterruptedException e) {
+                            // ignored
+                        }
+                    }
+                    latch.countDown();
+                }
+            }.start();
+            latch.await(10, TimeUnit.MINUTES);
+            assertNotNull(FDroidApp.repo.address);
+
+            LocalRepoService.runProcess(context, new String[]{context.getPackageName()});
+            Log.i(TAG, "REPO: " + FDroidApp.repo);
+            File indexJarFile = LocalRepoManager.get(context).getIndexJar();
+            assertTrue(indexJarFile.isFile());
+
+            localHttpd = new LocalHTTPD(
+                    context,
+                    null,
+                    FDroidApp.port,
+                    LocalRepoManager.get(context).getWebRoot(),
+                    false);
+            localHttpd.start();
+            Thread.sleep(100); // give the server some tine to start.
+            assertTrue(localHttpd.isAlive());
+
+            LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context);
+            Certificate localCert = localRepoKeyStore.getCertificate();
+            String signingCert = Hasher.hex(localCert);
+            assertFalse(TextUtils.isEmpty(signingCert));
+            assertFalse(TextUtils.isEmpty(Utils.calcFingerprint(localCert)));
+
+            Repo repoToDelete = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address);
+            while (repoToDelete != null) {
+                Log.d(TAG, "Removing old test swap repo matching this one: " + repoToDelete.address);
+                RepoProvider.Helper.remove(context, repoToDelete.getId());
+                repoToDelete = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address);
+            }
+
+            ContentValues values = new ContentValues(4);
+            values.put(Schema.RepoTable.Cols.SIGNING_CERT, signingCert);
+            values.put(Schema.RepoTable.Cols.ADDRESS, FDroidApp.repo.address);
+            values.put(Schema.RepoTable.Cols.NAME, FDroidApp.repo.name);
+            values.put(Schema.RepoTable.Cols.IS_SWAP, true);
+            final String lastEtag = UUID.randomUUID().toString();
+            values.put(Schema.RepoTable.Cols.LAST_ETAG, lastEtag);
+            RepoProvider.Helper.insert(context, values);
+            Repo repo = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address);
+            assertTrue(repo.isSwap);
+            assertNotEquals(-1, repo.getId());
+            assertTrue(repo.name.startsWith(FDroidApp.repo.name));
+            assertEquals(lastEtag, repo.lastetag);
+            assertNull(repo.lastUpdated);
+
+            assertTrue(isPortInUse(FDroidApp.ipAddressString, FDroidApp.port));
+            Thread.sleep(100);
+            IndexUpdater updater = new IndexUpdater(context, repo);
+            updater.update();
+            assertTrue(updater.hasChanged());
+
+            repo = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address);
+            final Date lastUpdated = repo.lastUpdated;
+            assertTrue("repo lastUpdated should be updated", new Date(2019, 5, 13).compareTo(repo.lastUpdated) > 0);
+
+            App app = AppProvider.Helper.findSpecificApp(context.getContentResolver(),
+                    context.getPackageName(), repo.getId());
+            assertEquals(context.getPackageName(), app.packageName);
+
+            List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL);
+            assertEquals(1, apks.size());
+            for (Apk apk : apks) {
+                Log.i(TAG, "Apk: " + apk);
+                assertEquals(context.getPackageName(), apk.packageName);
+                assertEquals(BuildConfig.VERSION_NAME, apk.versionName);
+                assertEquals(BuildConfig.VERSION_CODE, apk.versionCode);
+                assertEquals(app.repoId, apk.repoId);
+            }
+
+            Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
+            mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
+            List<ResolveInfo> resolveInfoList = context.getPackageManager().queryIntentActivities(mainIntent, 0);
+            HashSet<String> packageNames = new HashSet<>();
+            for (ResolveInfo resolveInfo : resolveInfoList) {
+                if (!isSystemPackage(resolveInfo)) {
+                    Log.i(TAG, "resolveInfo: " + resolveInfo);
+                    packageNames.add(resolveInfo.activityInfo.packageName);
+                }
+            }
+            LocalRepoService.runProcess(context, packageNames.toArray(new String[0]));
+
+            updater = new IndexUpdater(context, repo);
+            updater.update();
+            assertTrue(updater.hasChanged());
+            assertTrue("repo lastUpdated should be updated", lastUpdated.compareTo(repo.lastUpdated) < 0);
+
+            for (String packageName : packageNames) {
+                assertNotNull(ApkProvider.Helper.findByPackageName(context, packageName));
+            }
+        } finally {
+            if (localHttpd != null) {
+                localHttpd.stop();
+            }
+        }
+        if (localHttpd != null) {
+            assertFalse(localHttpd.isAlive());
+        }
+    }
+
+    private boolean isPortInUse(String host, int port) {
+        boolean result = false;
+
+        try {
+            (new Socket(host, port)).close();
+            result = true;
+        } catch (IOException e) {
+            // Could not connect.
+            e.printStackTrace();
+        }
+        return result;
+    }
+
+    private boolean isSystemPackage(ResolveInfo resolveInfo) {
+        return (resolveInfo.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
+    }
+}
diff --git a/app/src/full/AndroidManifest.xml b/app/src/full/AndroidManifest.xml
index 70c2bbe0f..b4cfe4e2f 100644
--- a/app/src/full/AndroidManifest.xml
+++ b/app/src/full/AndroidManifest.xml
@@ -80,6 +80,9 @@
         <service
                 android:name=".localrepo.CacheSwapAppsService"
                 android:exported="false"/>
+        <service
+                android:name=".localrepo.LocalRepoService"
+                android:exported="false"/>
         <service
                 android:name=".localrepo.TreeUriScannerIntentService"
                 android:exported="false"/>
diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java b/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java
index 1f818c7fb..12ee3f23d 100644
--- a/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java
+++ b/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java
@@ -21,6 +21,8 @@ import org.fdroid.fdroid.Preferences;
 import org.fdroid.fdroid.Utils;
 import org.fdroid.fdroid.data.Apk;
 import org.fdroid.fdroid.data.App;
+import org.fdroid.fdroid.data.InstalledApp;
+import org.fdroid.fdroid.data.InstalledAppProvider;
 import org.fdroid.fdroid.data.SanitizedFile;
 import org.xmlpull.v1.XmlPullParserException;
 import org.xmlpull.v1.XmlPullParserFactory;
@@ -246,6 +248,10 @@ public final class LocalRepoManager {
         return xmlIndexJar;
     }
 
+    public File getWebRoot() {
+        return webRoot;
+    }
+
     public void deleteRepo() {
         deleteContents(repoDir);
     }
diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoService.java b/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoService.java
new file mode 100644
index 000000000..554af9c9a
--- /dev/null
+++ b/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoService.java
@@ -0,0 +1,163 @@
+package org.fdroid.fdroid.localrepo;
+
+import android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
+import android.support.v4.content.LocalBroadcastManager;
+import org.fdroid.fdroid.FDroidApp;
+import org.fdroid.fdroid.R;
+import org.fdroid.fdroid.Utils;
+import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.fdroid.fdroid.views.swap.SwapWorkflowActivity.PrepareSwapRepo.EXTRA_TYPE;
+import static org.fdroid.fdroid.views.swap.SwapWorkflowActivity.PrepareSwapRepo.TYPE_COMPLETE;
+import static org.fdroid.fdroid.views.swap.SwapWorkflowActivity.PrepareSwapRepo.TYPE_ERROR;
+import static org.fdroid.fdroid.views.swap.SwapWorkflowActivity.PrepareSwapRepo.TYPE_STATUS;
+
+/**
+ * Handles setting up and generating the local repo used to swap apps, including
+ * the {@code index.jar}, the symlinks to the shared APKs, etc.
+ * <p/>
+ * The work is done in a {@link Thread} so that new incoming {@code Intents}
+ * are not blocked by processing. A new {@code Intent} immediately nullifies
+ * the current state because it means the user has chosen a different set of
+ * apps.  That is also enforced here since new {@code Intent}s with the same
+ * {@link Set} of apps as the current one are ignored.  Having the
+ * {@code Thread} also makes it easy to kill work that is in progress.
+ */
+public class LocalRepoService extends IntentService {
+    public static final String TAG = "LocalRepoService";
+
+    public static final String ACTION_PROGRESS = "org.fdroid.fdroid.localrepo.LocalRepoService.action.PROGRESS";
+    public static final String ACTION_COMPLETE = "org.fdroid.fdroid.localrepo.LocalRepoService.action.COMPLETE";
+    public static final String ACTION_ERROR = "org.fdroid.fdroid.localrepo.LocalRepoService.action.ERROR";
+
+    public static final String EXTRA_MESSAGE = "org.fdroid.fdroid.localrepo.LocalRepoService.extra.MESSAGE";
+
+    public static final String ACTION_CREATE = "org.fdroid.fdroid.localrepo.action.CREATE";
+    public static final String EXTRA_PACKAGE_NAMES = "org.fdroid.fdroid.localrepo.extra.PACKAGE_NAMES";
+
+    private final HashSet<String> selectedApps = new HashSet<>();
+
+    private GenerateLocalRepoThread thread;
+
+    public LocalRepoService() {
+        super("LocalRepoService");
+    }
+
+    /**
+     * Creates a skeleton swap repo with only F-Droid itself in it
+     */
+    public static void create(Context context) {
+        create(context, Collections.singleton(context.getPackageName()));
+    }
+
+    /**
+     * Sets up the local repo with the included {@code packageNames}
+     */
+    public static void create(Context context, Set<String> packageNames) {
+        Intent intent = new Intent(context, LocalRepoService.class);
+        intent.setAction(ACTION_CREATE);
+        intent.putExtra(EXTRA_PACKAGE_NAMES, packageNames.toArray(new String[0]));
+        context.startService(intent);
+    }
+
+    @Override
+    protected void onHandleIntent(Intent intent) {
+        String[] packageNames = intent.getStringArrayExtra(EXTRA_PACKAGE_NAMES);
+        if (packageNames == null || packageNames.length == 0) {
+            Utils.debugLog(TAG, "no packageNames found, quiting");
+            return;
+        }
+
+        boolean changed = Collections.addAll(selectedApps, packageNames);
+        if (!changed) {
+            Utils.debugLog(TAG, "packageNames list unchanged, quiting");
+            return;
+        }
+
+        if (thread != null) {
+            thread.interrupt();
+        }
+        thread = new GenerateLocalRepoThread();
+        thread.start();
+    }
+
+    private class GenerateLocalRepoThread extends Thread {
+        private static final String TAG = "GenerateLocalRepoThread";
+
+        @Override
+        public void run() {
+            android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST);
+            runProcess(LocalRepoService.this, selectedApps);
+        }
+    }
+
+    public static void runProcess(Context context, Set<String> selectedApps) {
+        try {
+            final LocalRepoManager lrm = LocalRepoManager.get(context);
+            broadcast(context, ACTION_PROGRESS, R.string.deleting_repo);
+            lrm.deleteRepo();
+            for (String app : selectedApps) {
+                broadcast(context, ACTION_PROGRESS, context.getString(R.string.adding_apks_format, app));
+                lrm.addApp(context, app);
+            }
+            String urlString = Utils.getSharingUri(FDroidApp.repo).toString();
+            lrm.writeIndexPage(urlString);
+            broadcast(context, ACTION_PROGRESS, R.string.writing_index_jar);
+            lrm.writeIndexJar();
+            broadcast(context, ACTION_PROGRESS, R.string.linking_apks);
+            lrm.copyApksToRepo();
+            broadcast(context, ACTION_PROGRESS, R.string.copying_icons);
+            // run the icon copy without progress, its not a blocker
+            new Thread() {
+                @Override
+                public void run() {
+                    android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST);
+                    lrm.copyIconsToRepo();
+                }
+            }.start();
+
+            broadcast(context, ACTION_COMPLETE, null);
+        } catch (IOException | XmlPullParserException | LocalRepoKeyStore.InitException e) {
+            broadcast(context, ACTION_ERROR, e.getLocalizedMessage());
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Translate Android style broadcast {@link Intent}s to {@code PrepareSwapRepo}
+     */
+    static void broadcast(Context context, String action, String message) {
+        Intent intent = new Intent(context, SwapWorkflowActivity.class);
+        intent.setAction(SwapWorkflowActivity.PrepareSwapRepo.ACTION);
+        switch (action) {
+            case ACTION_PROGRESS:
+                intent.putExtra(EXTRA_TYPE, TYPE_STATUS);
+                break;
+            case ACTION_COMPLETE:
+                intent.putExtra(EXTRA_TYPE, TYPE_COMPLETE);
+                break;
+            case ACTION_ERROR:
+                intent.putExtra(EXTRA_TYPE, TYPE_ERROR);
+                break;
+            default:
+                throw new IllegalArgumentException("unsupported action");
+        }
+        if (message != null) {
+            Utils.debugLog(TAG, "Preparing swap: " + message);
+            intent.putExtra(EXTRA_MESSAGE, message);
+        }
+        LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
+    }
+
+    static void broadcast(Context context, String action, int resId) {
+        broadcast(context, action, context.getString(resId));
+    }
+}
diff --git a/app/src/full/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/app/src/full/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
index ee92c4dca..d40a35ae1 100644
--- a/app/src/full/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
+++ b/app/src/full/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
@@ -12,7 +12,6 @@ import android.content.Intent;
 import android.content.ServiceConnection;
 import android.net.Uri;
 import android.net.wifi.WifiManager;
-import android.os.AsyncTask;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -51,19 +50,16 @@ import org.fdroid.fdroid.data.App;
 import org.fdroid.fdroid.data.NewRepoConfig;
 import org.fdroid.fdroid.installer.InstallManagerService;
 import org.fdroid.fdroid.installer.Installer;
-import org.fdroid.fdroid.localrepo.LocalRepoManager;
+import org.fdroid.fdroid.localrepo.LocalRepoService;
 import org.fdroid.fdroid.localrepo.SwapService;
 import org.fdroid.fdroid.localrepo.SwapView;
 import org.fdroid.fdroid.localrepo.peers.Peer;
 import org.fdroid.fdroid.net.BluetoothDownloader;
 import org.fdroid.fdroid.net.HttpDownloader;
 
-import java.util.Arrays;
 import java.util.Date;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Map;
-import java.util.Set;
 import java.util.Timer;
 import java.util.TimerTask;
 
@@ -103,7 +99,6 @@ public class SwapWorkflowActivity extends AppCompatActivity {
     private Toolbar toolbar;
     private SwapView currentView;
     private boolean hasPreparedLocalRepo;
-    private PrepareSwapRepo updateSwappableAppsTask;
     private NewRepoConfig confirmSwapConfig;
     private LocalBroadcastManager localBroadcastManager;
     private WifiManager wifiManager;
@@ -504,12 +499,7 @@ public class SwapWorkflowActivity extends AppCompatActivity {
         // as we are starting over now.
         getService().swapWith(null);
 
-        if (!getService().isEnabled()) {
-            if (!LocalRepoManager.get(this).getIndexJar().exists()) {
-                Utils.debugLog(TAG, "Preparing initial repo with only F-Droid, until we have allowed the user to configure their own repo.");
-                new PrepareInitialSwapRepo().execute();
-            }
-        }
+        LocalRepoService.create(this);
 
         inflateSwapView(R.layout.swap_start_swap);
     }
@@ -591,30 +581,34 @@ public class SwapWorkflowActivity extends AppCompatActivity {
         ((FDroidApp) getApplication()).sendViaBluetooth(this, Activity.RESULT_OK, BuildConfig.APPLICATION_ID);
     }
 
-    // TODO: Figure out whether they have changed since last time UpdateAsyncTask was run.
-    // If the local repo is running, then we can ask it what apps it is swapping and compare with that.
-    // Otherwise, probably will need to scan the file system.
+    /**
+     * TODO: Figure out whether they have changed since last time LocalRepoService
+     * was run.  If the local repo is running, then we can ask it what apps it is
+     * swapping and compare with that. Otherwise, probably will need to scan the
+     * file system.
+     */
     public void onAppsSelected() {
-        if (updateSwappableAppsTask == null && !hasPreparedLocalRepo) {
-            updateSwappableAppsTask = new PrepareSwapRepo(getService().getAppsToSwap());
-            updateSwappableAppsTask.execute();
+        if (hasPreparedLocalRepo) {
+            onLocalRepoPrepared();
+        } else {
+            LocalRepoService.create(this, getService().getAppsToSwap());
             getService().setCurrentView(R.layout.swap_connecting);
             inflateSwapView(R.layout.swap_connecting);
-        } else {
-            onLocalRepoPrepared();
         }
     }
 
     /**
      * Once the UpdateAsyncTask has finished preparing our repository index, we can
      * show the next screen to the user. This will be one of two things:
-     * * If we directly selected a peer to swap with initially, we will skip straight to getting
-     * the list of apps from that device.
-     * * Alternatively, if we didn't have a person to connect to, and instead clicked "Scan QR Code",
-     * then we want to show a QR code or NFC dialog.
+     * <ol>
+     * <li>If we directly selected a peer to swap with initially, we will skip straight to getting
+     * the list of apps from that device.</li>
+     * <li>Alternatively, if we didn't have a person to connect to, and instead clicked "Scan QR Code",
+     * then we want to show a QR code or NFC dialog.</li>
+     * </ol>
      */
     public void onLocalRepoPrepared() {
-        updateSwappableAppsTask = null;
+        // TODO ditch this, use a message from LocalRepoService.  Maybe?
         hasPreparedLocalRepo = true;
         if (getService().isConnectingWithPeer()) {
             startSwappingWithPeer();
@@ -637,6 +631,7 @@ public class SwapWorkflowActivity extends AppCompatActivity {
         // during the wifi qr code being shown too.
         boolean nfcMessageReady = NfcHelper.setPushMessage(this, Utils.getSharingUri(FDroidApp.repo));
 
+        // TODO move all swap-specific preferences to a SharedPreferences instance for SwapWorkflowActivity
         if (Preferences.get().showNfcDuringSwap() && nfcMessageReady) {
             inflateSwapView(R.layout.swap_nfc);
             return true;
@@ -779,82 +774,13 @@ public class SwapWorkflowActivity extends AppCompatActivity {
         service.getBluetoothSwap().startInBackground();  // TODO replace with Intent to SwapService
     }
 
-    class PrepareInitialSwapRepo extends PrepareSwapRepo {
-        PrepareInitialSwapRepo() {
-            super(new HashSet<>(Arrays.asList(new String[]{BuildConfig.APPLICATION_ID})));
-        }
-    }
-
-    class PrepareSwapRepo extends AsyncTask<Void, Void, Void> {
-
+    public class PrepareSwapRepo {
         public static final String ACTION = "PrepareSwapRepo.Action";
         public static final String EXTRA_MESSAGE = "PrepareSwapRepo.Status.Message";
         public static final String EXTRA_TYPE = "PrepareSwapRepo.Action.Type";
         public static final int TYPE_STATUS = 0;
         public static final int TYPE_COMPLETE = 1;
         public static final int TYPE_ERROR = 2;
-
-        @NonNull
-        protected final Set<String> selectedApps;
-
-        @NonNull
-        protected final Uri sharingUri;
-
-        @NonNull
-        protected final Context context;
-
-        PrepareSwapRepo(@NonNull Set<String> apps) {
-            context = SwapWorkflowActivity.this;
-            selectedApps = apps;
-            sharingUri = Utils.getSharingUri(FDroidApp.repo);
-        }
-
-        private void broadcast(int type) {
-            broadcast(type, null);
-        }
-
-        private void broadcast(int type, String message) {
-            Intent intent = new Intent(ACTION);
-            intent.putExtra(EXTRA_TYPE, type);
-            if (message != null) {
-                Utils.debugLog(TAG, "Preparing swap: " + message);
-                intent.putExtra(EXTRA_MESSAGE, message);
-            }
-            LocalBroadcastManager.getInstance(SwapWorkflowActivity.this).sendBroadcast(intent);
-        }
-
-        @Override
-        protected Void doInBackground(Void... params) {
-            try {
-                final LocalRepoManager lrm = LocalRepoManager.get(context);
-                broadcast(TYPE_STATUS, getString(R.string.deleting_repo));
-                lrm.deleteRepo();
-                for (String app : selectedApps) {
-                    broadcast(TYPE_STATUS, String.format(getString(R.string.adding_apks_format), app));
-                    lrm.addApp(context, app);
-                }
-                lrm.writeIndexPage(sharingUri.toString());
-                broadcast(TYPE_STATUS, getString(R.string.writing_index_jar));
-                lrm.writeIndexJar();
-                broadcast(TYPE_STATUS, getString(R.string.linking_apks));
-                lrm.copyApksToRepo();
-                broadcast(TYPE_STATUS, getString(R.string.copying_icons));
-                // run the icon copy without progress, its not a blocker
-                new Thread() {
-                    @Override
-                    public void run() {
-                        android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
-                        lrm.copyIconsToRepo();
-                    }
-                }.start();
-
-                broadcast(TYPE_COMPLETE);
-            } catch (Exception e) {
-                broadcast(TYPE_ERROR);
-                Log.e(TAG, "", e);
-            }
-            return null;
-        }
     }
 
     /**
diff --git a/app/src/testFull/java/org/fdroid/fdroid/data/ShadowApp.java b/app/src/testFull/java/org/fdroid/fdroid/data/ShadowApp.java
new file mode 100644
index 000000000..a9534989f
--- /dev/null
+++ b/app/src/testFull/java/org/fdroid/fdroid/data/ShadowApp.java
@@ -0,0 +1,14 @@
+package org.fdroid.fdroid.data;
+
+import android.content.Context;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+@Implements(App.class)
+public class ShadowApp extends ValueObject {
+
+    @Implementation
+    protected static int[] getMinTargetMaxSdkVersions(Context context, String packageName) {
+        return new int[]{10, 23, Apk.SDK_VERSION_MAX_VALUE};
+    }
+}
diff --git a/app/src/testFull/java/org/fdroid/fdroid/updater/SwapRepoTest.java b/app/src/testFull/java/org/fdroid/fdroid/updater/SwapRepoTest.java
new file mode 100644
index 000000000..1436c5040
--- /dev/null
+++ b/app/src/testFull/java/org/fdroid/fdroid/updater/SwapRepoTest.java
@@ -0,0 +1,187 @@
+package org.fdroid.fdroid.updater;
+
+import android.content.ContentResolver;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.text.TextUtils;
+import org.apache.commons.net.util.SubnetUtils;
+import org.fdroid.fdroid.FDroidApp;
+import org.fdroid.fdroid.Hasher;
+import org.fdroid.fdroid.IndexUpdater;
+import org.fdroid.fdroid.Preferences;
+import org.fdroid.fdroid.TestUtils;
+import org.fdroid.fdroid.Utils;
+import org.fdroid.fdroid.data.Apk;
+import org.fdroid.fdroid.data.ApkProvider;
+import org.fdroid.fdroid.data.AppProvider;
+import org.fdroid.fdroid.data.DBHelper;
+import org.fdroid.fdroid.data.Repo;
+import org.fdroid.fdroid.data.RepoProvider;
+import org.fdroid.fdroid.data.Schema;
+import org.fdroid.fdroid.data.ShadowApp;
+import org.fdroid.fdroid.data.TempAppProvider;
+import org.fdroid.fdroid.localrepo.LocalRepoKeyStore;
+import org.fdroid.fdroid.localrepo.LocalRepoManager;
+import org.fdroid.fdroid.localrepo.LocalRepoService;
+import org.fdroid.fdroid.net.LocalHTTPD;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowContentResolver;
+import org.robolectric.shadows.ShadowLog;
+import org.robolectric.shadows.ShadowPackageManager;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.cert.Certificate;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.robolectric.Shadows.shadowOf;
+
+/**
+ * This test almost works, it needs to have the {@link android.content.ContentProvider}
+ * and {@link ContentResolver} stuff worked out.  It currently fails as
+ * {@code updater.update()}.
+ */
+@Ignore
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = ShadowApp.class)
+public class SwapRepoTest {
+
+    private LocalHTTPD localHttpd;
+
+    protected ShadowContentResolver shadowContentResolver;
+    protected ContentResolver contentResolver;
+    protected ContextWrapper context;
+
+    @Before
+    public void setUp() {
+        ShadowLog.stream = System.out;
+
+        contentResolver = RuntimeEnvironment.application.getContentResolver();
+        shadowContentResolver = Shadows.shadowOf(contentResolver);
+        context = new ContextWrapper(RuntimeEnvironment.application.getApplicationContext()) {
+            @Override
+            public ContentResolver getContentResolver() {
+                return contentResolver;
+            }
+        };
+
+        TestUtils.registerContentProvider(ApkProvider.getAuthority(), ApkProvider.class);
+        TestUtils.registerContentProvider(AppProvider.getAuthority(), AppProvider.class);
+        TestUtils.registerContentProvider(RepoProvider.getAuthority(), RepoProvider.class);
+        TestUtils.registerContentProvider(TempAppProvider.getAuthority(), TempAppProvider.class);
+
+        Preferences.setupForTests(context);
+    }
+
+    @After
+    public final void tearDownBase() {
+        DBHelper.clearDbHelperSingleton();
+    }
+
+    /**
+     * @see org.fdroid.fdroid.net.WifiStateChangeService.WifiInfoThread#run()
+     */
+    @Test
+    public void testSwap()
+            throws IOException, LocalRepoKeyStore.InitException, IndexUpdater.UpdateException, InterruptedException {
+
+        PackageManager packageManager = context.getPackageManager();
+        ShadowPackageManager shadowPackageManager = shadowOf(packageManager);
+        ApplicationInfo appInfo = new ApplicationInfo();
+        appInfo.flags = 0;
+        appInfo.packageName = context.getPackageName();
+        appInfo.minSdkVersion = 10;
+        appInfo.targetSdkVersion = 23;
+        appInfo.sourceDir = getClass().getClassLoader().getResource("F-Droid.apk").getPath();
+        appInfo.publicSourceDir = getClass().getClassLoader().getResource("F-Droid.apk").getPath();
+        System.out.println("appInfo.sourceDir " + appInfo.sourceDir);
+        appInfo.name = "F-Droid";
+
+        PackageInfo packageInfo = new PackageInfo();
+        packageInfo.packageName = appInfo.packageName;
+        packageInfo.applicationInfo = appInfo;
+        packageInfo.versionCode = 1002001;
+        packageInfo.versionName = "1.2-fake";
+        shadowPackageManager.addPackage(packageInfo);
+
+        try {
+            FDroidApp.initWifiSettings();
+            FDroidApp.ipAddressString = "127.0.0.1";
+            FDroidApp.subnetInfo = new SubnetUtils("127.0.0.0/8").getInfo();
+            FDroidApp.repo.name = "test";
+            FDroidApp.repo.address = "http://" + FDroidApp.ipAddressString + ":" + FDroidApp.port + "/fdroid/repo";
+
+            LocalRepoService.runProcess(context, new String[]{context.getPackageName()});
+            File indexJarFile = LocalRepoManager.get(context).getIndexJar();
+            System.out.println("indexJarFile:" + indexJarFile);
+            assertTrue(indexJarFile.isFile());
+
+            localHttpd = new LocalHTTPD(
+                    context,
+                    FDroidApp.ipAddressString,
+                    FDroidApp.port,
+                    LocalRepoManager.get(context).getWebRoot(),
+                    false);
+            localHttpd.start();
+            Thread.sleep(100); // give the server some tine to start.
+            assertTrue(localHttpd.isAlive());
+
+            LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context);
+            Certificate localCert = localRepoKeyStore.getCertificate();
+            String signingCert = Hasher.hex(localCert);
+            assertFalse(TextUtils.isEmpty(signingCert));
+            assertFalse(TextUtils.isEmpty(Utils.calcFingerprint(localCert)));
+
+            Repo repo = MultiIndexUpdaterTest.createRepo(FDroidApp.repo.name, FDroidApp.repo.address,
+                    context, signingCert);
+            IndexUpdater updater = new IndexUpdater(context, repo);
+            updater.update();
+            assertTrue(updater.hasChanged());
+            updater.processDownloadedFile(indexJarFile);
+
+            boolean foundRepo = false;
+            for (Repo repoFromDb : RepoProvider.Helper.all(context)) {
+                if (TextUtils.equals(repo.address, repoFromDb.address)) {
+                    foundRepo = true;
+                    repo = repoFromDb;
+                }
+            }
+            assertTrue(foundRepo);
+
+            assertNotEquals(-1, repo.getId());
+            List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL);
+            assertEquals(1, apks.size());
+            for (Apk apk : apks) {
+                System.out.println(apk);
+            }
+            //MultiIndexUpdaterTest.assertApksExist(apks, context.getPackageName(), new int[]{BuildConfig.VERSION_CODE});
+            Thread.sleep(10000);
+        } finally {
+            if (localHttpd != null) {
+                localHttpd.stop();
+            }
+        }
+    }
+
+    class TestLocalRepoService extends LocalRepoService {
+        @Override
+        protected void onHandleIntent(Intent intent) {
+            super.onHandleIntent(intent);
+        }
+    }
+}
\ No newline at end of file