diff --git a/F-Droid/AndroidManifest.xml b/F-Droid/AndroidManifest.xml
index 42b92faf2..083f0cfc0 100644
--- a/F-Droid/AndroidManifest.xml
+++ b/F-Droid/AndroidManifest.xml
@@ -456,7 +456,8 @@
 
         <service android:name=".UpdateService" />
         <service android:name=".net.WifiStateChangeService" />
-        <service android:name=".localrepo.LocalRepoService" />
+        <service android:name=".localrepo.LocalRepoWifiService" />
+        <service android:name=".localrepo.LocalRepoProxyService" />
     </application>
 
 </manifest>
diff --git a/F-Droid/res/values/styles.xml b/F-Droid/res/values/styles.xml
index 9626b410e..0a2dcc8b2 100644
--- a/F-Droid/res/values/styles.xml
+++ b/F-Droid/res/values/styles.xml
@@ -43,15 +43,20 @@
         <item name="android:background">@color/white</item>
     </style>
 
-    <style name="SwapTheme.BluetoothDeviceList" parent="AppThemeLightWithDarkActionBar">
+    <style name="SwapTheme.StartSwap.Text" parent="@style/SwapTheme.StartSwap">
+    </style>
+
+    <style name="SwapTheme.BluetoothDeviceList" parent="@style/SwapTheme.StartSwap">
     </style>
 
     <style name="SwapTheme.BluetoothDeviceList.ListItem" parent="AppThemeLightWithDarkActionBar">
     </style>
 
-    <style name="SwapTheme.BluetoothDeviceList.Heading" parent="@style/SwapTheme.Wizard.MainText">
+    <style name="SwapTheme.BluetoothDeviceList.Text" parent="@style/SwapTheme.BluetoothDeviceList">
+    </style>
+
+    <style name="SwapTheme.BluetoothDeviceList.Heading" parent="@style/SwapTheme.BluetoothDeviceList.Text">
         <item name="android:textSize">32.5dp</item> <!-- 58px * 96dpi / 160dpi = 32.5sp -->
-        <item name="android:textColor">#222</item>
     </style>
 
     <style name="SwapTheme.AppList" parent="AppThemeLightWithDarkActionBar">
diff --git a/F-Droid/src/org/fdroid/fdroid/FDroidApp.java b/F-Droid/src/org/fdroid/fdroid/FDroidApp.java
index 6f99b3106..7efa74615 100644
--- a/F-Droid/src/org/fdroid/fdroid/FDroidApp.java
+++ b/F-Droid/src/org/fdroid/fdroid/FDroidApp.java
@@ -52,7 +52,9 @@ import org.fdroid.fdroid.compat.PRNGFixes;
 import org.fdroid.fdroid.data.AppProvider;
 import org.fdroid.fdroid.data.InstalledAppCacheUpdater;
 import org.fdroid.fdroid.data.Repo;
+import org.fdroid.fdroid.localrepo.LocalRepoProxyService;
 import org.fdroid.fdroid.localrepo.LocalRepoService;
+import org.fdroid.fdroid.localrepo.LocalRepoWifiService;
 import org.fdroid.fdroid.net.IconDownloader;
 import org.fdroid.fdroid.net.WifiStateChangeService;
 
@@ -73,8 +75,6 @@ public class FDroidApp extends Application {
 
     // Leaving the fully qualified class name here to help clarify the difference between spongy/bouncy castle.
     private static final org.spongycastle.jce.provider.BouncyCastleProvider spongyCastleProvider;
-    private static Messenger localRepoServiceMessenger = null;
-    private static boolean localRepoServiceIsBound = false;
 
     private static final String TAG = "FDroidApp";
 
@@ -91,6 +91,9 @@ public class FDroidApp extends Application {
 
     private static Theme curTheme = Theme.dark;
 
+    public static final LocalRepoServiceManager localRepoWifi = new LocalRepoServiceManager(LocalRepoWifiService.class);
+    public static final LocalRepoServiceManager localRepoProxy = new LocalRepoServiceManager(LocalRepoProxyService.class);
+
     public void reloadTheme() {
         curTheme = Theme.valueOf(PreferenceManager
                 .getDefaultSharedPreferences(getBaseContext())
@@ -302,52 +305,72 @@ public class FDroidApp extends Application {
         }
     }
 
-    private static final ServiceConnection serviceConnection = new ServiceConnection() {
-        @Override
-        public void onServiceConnected(ComponentName className, IBinder service) {
-            localRepoServiceMessenger = new Messenger(service);
-        }
-
-        @Override
-        public void onServiceDisconnected(ComponentName className) {
-            localRepoServiceMessenger = null;
-        }
-    };
-
-    public static void startLocalRepoService(Context context) {
-        if (!localRepoServiceIsBound) {
-            Context app = context.getApplicationContext();
-            Intent service = new Intent(app, LocalRepoService.class);
-            localRepoServiceIsBound = app.bindService(service, serviceConnection, Context.BIND_AUTO_CREATE);
-            if (localRepoServiceIsBound)
-                app.startService(service);
-        }
-    }
-
-    public static void stopLocalRepoService(Context context) {
-        Context app = context.getApplicationContext();
-        if (localRepoServiceIsBound) {
-            app.unbindService(serviceConnection);
-            localRepoServiceIsBound = false;
-        }
-        app.stopService(new Intent(app, LocalRepoService.class));
-    }
-
     /**
-     * Handles checking if the {@link LocalRepoService} is running, and only restarts it if it was running.
+     * Helper class to encapsulate functionality relating to local repo service starting/stopping/restarting.
+     * It used to live as static methods in FDroidApp, but once there were two types of local repos which
+     * could get started (wifi and local proxy for bluetooth) then it got a bit messy. This allows us to
+     * support managing both of these services through two static variables
+     * {@link org.fdroid.fdroid.FDroidApp#localRepoProxy} and {@link org.fdroid.fdroid.FDroidApp#localRepoWifi}.
      */
-    public static void restartLocalRepoServiceIfRunning() {
-        if (localRepoServiceMessenger != null) {
-            try {
-                Message msg = Message.obtain(null, LocalRepoService.RESTART, LocalRepoService.RESTART, 0);
-                localRepoServiceMessenger.send(msg);
-            } catch (RemoteException e) {
-                e.printStackTrace();
+    public static class LocalRepoServiceManager {
+
+        private Messenger localRepoServiceMessenger = null;
+        private boolean localRepoServiceIsBound = false;
+
+        private final Class<?extends LocalRepoService> serviceType;
+
+        public LocalRepoServiceManager(Class<?extends LocalRepoService> serviceType) {
+            this.serviceType = serviceType;
+        }
+
+        private ServiceConnection serviceConnection = new ServiceConnection() {
+            @Override
+            public void onServiceConnected(ComponentName className, IBinder service) {
+                localRepoServiceMessenger = new Messenger(service);
+            }
+
+            @Override
+            public void onServiceDisconnected(ComponentName className) {
+                localRepoServiceMessenger = null;
+            }
+        };
+
+        public void start(Context context) {
+            if (!localRepoServiceIsBound) {
+                Context app = context.getApplicationContext();
+                Intent service = new Intent(app, serviceType);
+                localRepoServiceIsBound = app.bindService(service, serviceConnection, Context.BIND_AUTO_CREATE);
+                if (localRepoServiceIsBound)
+                    app.startService(service);
             }
         }
-    }
 
-    public static boolean isLocalRepoServiceRunning() {
-        return localRepoServiceIsBound;
+        public void stop(Context context) {
+            Context app = context.getApplicationContext();
+            if (localRepoServiceIsBound) {
+                app.unbindService(serviceConnection);
+                localRepoServiceIsBound = false;
+            }
+            app.stopService(new Intent(app, serviceType));
+        }
+
+        /**
+         * Handles checking if the {@link LocalRepoService} is running, and only restarts it if it was running.
+         */
+        public void restartIfRunning() {
+            if (localRepoServiceMessenger != null) {
+                try {
+                    Message msg = Message.obtain(null, LocalRepoService.RESTART, LocalRepoService.RESTART, 0);
+                    localRepoServiceMessenger.send(msg);
+                } catch (RemoteException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+
+        public boolean isRunning() {
+            return localRepoServiceIsBound;
+        }
+
     }
 }
diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoService.java b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoService.java
index 8e835e2b6..7db6cca4c 100644
--- a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoService.java
+++ b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoService.java
@@ -5,11 +5,7 @@ import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.Service;
-import android.content.BroadcastReceiver;
-import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.AsyncTask;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -20,8 +16,6 @@ import android.support.v4.content.LocalBroadcastManager;
 import android.util.Log;
 
 import org.fdroid.fdroid.FDroidApp;
-import org.fdroid.fdroid.Preferences;
-import org.fdroid.fdroid.Preferences.ChangeListener;
 import org.fdroid.fdroid.R;
 import org.fdroid.fdroid.Utils;
 import org.fdroid.fdroid.net.LocalHTTPD;
@@ -30,13 +24,13 @@ import org.fdroid.fdroid.views.swap.SwapActivity;
 
 import java.io.IOException;
 import java.net.BindException;
-import java.util.HashMap;
 import java.util.Random;
 
 import javax.jmdns.JmDNS;
 import javax.jmdns.ServiceInfo;
 
-public class LocalRepoService extends Service {
+public abstract class LocalRepoService extends Service {
+
     private static final String TAG = "LocalRepoService";
 
     public static final String STATE = "org.fdroid.fdroid.action.LOCAL_REPO_STATE";
@@ -50,9 +44,7 @@ public class LocalRepoService extends Service {
     private final int NOTIFICATION = R.string.local_repo_running;
 
     private Handler webServerThreadHandler = null;
-    private LocalHTTPD localHttpd;
-    private JmDNS jmdns;
-    private ServiceInfo pairService;
+    protected LocalHTTPD localHttpd;
 
     public static final int START = 1111111;
     public static final int STOP = 12345678;
@@ -97,42 +89,6 @@ public class LocalRepoService extends Service {
         }
     }
 
-    private final BroadcastReceiver onWifiChange = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent i) {
-            stopNetworkServices();
-            startNetworkServices();
-        }
-    };
-
-    private ChangeListener localRepoBonjourChangeListener = new ChangeListener() {
-        @Override
-        public void onPreferenceChange() {
-            if (localHttpd.isAlive())
-                if (Preferences.get().isLocalRepoBonjourEnabled())
-                    registerMDNSService();
-                else
-                    unregisterMDNSService();
-        }
-    };
-
-    private final ChangeListener localRepoHttpsChangeListener = new ChangeListener() {
-        @Override
-        public void onPreferenceChange() {
-            Log.i(TAG, "onPreferenceChange");
-            if (localHttpd.isAlive()) {
-                new AsyncTask<Void, Void, Void>() {
-                    @Override
-                    protected Void doInBackground(Void... params) {
-                        stopNetworkServices();
-                        startNetworkServices();
-                        return null;
-                    }
-                }.execute();
-            }
-        }
-    };
-
     private void showNotification() {
         notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
         // launch LocalRepoActivity if the user selects this notification
@@ -150,12 +106,10 @@ public class LocalRepoService extends Service {
 
     @Override
     public void onCreate() {
+        super.onCreate();
+
         showNotification();
         startNetworkServices();
-        Preferences.get().registerLocalRepoBonjourListeners(localRepoBonjourChangeListener);
-
-        LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange,
-                new IntentFilter(WifiStateChangeService.BROADCAST));
     }
 
     @Override
@@ -167,6 +121,7 @@ public class LocalRepoService extends Service {
 
     @Override
     public void onDestroy() {
+        super.onDestroy();
         new Thread() {
             public void run() {
                 stopNetworkServices();
@@ -174,8 +129,6 @@ public class LocalRepoService extends Service {
         }.start();
 
         notificationManager.cancel(NOTIFICATION);
-        LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange);
-        Preferences.get().unregisterLocalRepoBonjourListeners(localRepoBonjourChangeListener);
     }
 
     @Override
@@ -183,26 +136,40 @@ public class LocalRepoService extends Service {
         return messenger.getBinder();
     }
 
-    private void startNetworkServices() {
+    /**
+     * Called immediately _after_ the webserver is started.
+     */
+    protected abstract void onStartNetworkServices();
+
+    /**
+     * Called immediately _before_ the webserver is stopped.
+     */
+    protected abstract void onStopNetworkServices();
+
+    /**
+     * Whether or not this particular version of LocalRepoService requires a HTTPS
+     * connection. In the local proxy instance, it will not require it, but in the
+     * wifi setting, it should use whatever preference the user selected.
+     */
+    protected abstract boolean useHttps();
+
+    protected void startNetworkServices() {
         Log.d(TAG, "Starting local repo network services");
         startWebServer();
-        if (Preferences.get().isLocalRepoBonjourEnabled())
-            registerMDNSService();
-        Preferences.get().registerLocalRepoHttpsListeners(localRepoHttpsChangeListener);
+
+        onStartNetworkServices();
     }
 
-    private void stopNetworkServices() {
-        Log.d(TAG, "Stopping local repo network services");
-        Preferences.get().unregisterLocalRepoHttpsListeners(localRepoHttpsChangeListener);
-
-        Log.d(TAG, "Unregistering MDNS service...");
-        unregisterMDNSService();
+    protected void stopNetworkServices() {
+        onStopNetworkServices();
 
         Log.d(TAG, "Stopping web server...");
         stopWebServer();
     }
 
-    private void startWebServer() {
+    protected abstract String getIpAddressToBindTo();
+
+    protected void startWebServer() {
         Runnable webServer = new Runnable() {
             // Tell Eclipse this is not a leak because of Looper use.
             @SuppressLint("HandlerLeak")
@@ -210,8 +177,10 @@ public class LocalRepoService extends Service {
             public void run() {
                 localHttpd = new LocalHTTPD(
                         LocalRepoService.this,
+                        getIpAddressToBindTo(),
+                        FDroidApp.port,
                         getFilesDir(),
-                        Preferences.get().isLocalRepoHttpsEnabled());
+                        useHttps());
 
                 Looper.prepare(); // must be run before creating a Handler
                 webServerThreadHandler = new Handler() {
@@ -254,59 +223,5 @@ public class LocalRepoService extends Service {
         LocalBroadcastManager.getInstance(LocalRepoService.this).sendBroadcast(intent);
     }
 
-    private void registerMDNSService() {
-        new Thread(new Runnable() {
-            @Override
-            public void run() {
-                /*
-                 * a ServiceInfo can only be registered with a single instance
-                 * of JmDNS, and there is only ever a single LocalHTTPD port to
-                 * advertise anyway.
-                 */
-                if (pairService != null || jmdns != null)
-                    clearCurrentMDNSService();
-                String repoName = Preferences.get().getLocalRepoName();
-                HashMap<String, String> values = new HashMap<>();
-                values.put("path", "/fdroid/repo");
-                values.put("name", repoName);
-                values.put("fingerprint", FDroidApp.repo.fingerprint);
-                String type;
-                if (Preferences.get().isLocalRepoHttpsEnabled()) {
-                    values.put("type", "fdroidrepos");
-                    type = "_https._tcp.local.";
-                } else {
-                    values.put("type", "fdroidrepo");
-                    type = "_http._tcp.local.";
-                }
-                try {
-                    pairService = ServiceInfo.create(type, repoName, FDroidApp.port, 0, 0, values);
-                    jmdns = JmDNS.create();
-                    jmdns.registerService(pairService);
-                } catch (IOException e) {
-                    Log.e(TAG, "Error while registering jmdns service: " + e);
-                    Log.e(TAG, Log.getStackTraceString(e));
-                }
-            }
-        }).start();
-    }
-
-    private void unregisterMDNSService() {
-        if (localRepoBonjourChangeListener != null) {
-            Preferences.get().unregisterLocalRepoBonjourListeners(localRepoBonjourChangeListener);
-            localRepoBonjourChangeListener = null;
-        }
-        clearCurrentMDNSService();
-    }
-
-    private void clearCurrentMDNSService() {
-        if (jmdns != null) {
-            if (pairService != null) {
-                jmdns.unregisterService(pairService);
-                pairService = null;
-            }
-            jmdns.unregisterAllServices();
-            Utils.closeQuietly(jmdns);
-            jmdns = null;
-        }
-    }
 }
+
diff --git a/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java
index c1a6a3650..af5e5bef5 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java
@@ -2,9 +2,9 @@ package org.fdroid.fdroid.net;
 
 import android.content.Context;
 import android.util.Log;
-
 import org.fdroid.fdroid.Preferences;
 
+import javax.net.ssl.SSLHandshakeException;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -16,8 +16,6 @@ import java.net.Proxy;
 import java.net.SocketAddress;
 import java.net.URL;
 
-import javax.net.ssl.SSLHandshakeException;
-
 public class HttpDownloader extends Downloader {
     private static final String TAG = "HttpDownloader";
 
@@ -26,6 +24,7 @@ public class HttpDownloader extends Downloader {
 
     protected HttpURLConnection connection;
     private int statusCode = -1;
+    private boolean onlyStream = false;
 
     // The context is required for opening the file to write to.
     HttpDownloader(String source, File destFile)
@@ -39,11 +38,22 @@ public class HttpDownloader extends Downloader {
      * you are done*.
      * @see org.fdroid.fdroid.net.Downloader#getFile()
      */
-    HttpDownloader(String source, Context ctx) throws IOException {
+    public HttpDownloader(String source, Context ctx) throws IOException {
         super(ctx);
         sourceUrl = new URL(source);
     }
 
+    /**
+     * Calling this makes this downloader not download a file. Instead, it will
+     * only stream the file through the {@link HttpDownloader#getInputStream()}
+     * @return
+     */
+    public HttpDownloader streamDontDownload()
+    {
+        onlyStream = true;
+        return this;
+    }
+
     @Override
     public InputStream getInputStream() throws IOException {
         setupConnection();
diff --git a/F-Droid/src/org/fdroid/fdroid/net/LocalHTTPD.java b/F-Droid/src/org/fdroid/fdroid/net/LocalHTTPD.java
index 6b403e257..c67f71077 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/LocalHTTPD.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/LocalHTTPD.java
@@ -36,8 +36,8 @@ public class LocalHTTPD extends NanoHTTPD {
     private final File webRoot;
     private final boolean logRequests;
 
-    public LocalHTTPD(Context context, File webRoot, boolean useHttps) {
-        super(FDroidApp.ipAddressString, FDroidApp.port);
+    public LocalHTTPD(Context context, String hostname, int port, File webRoot, boolean useHttps) {
+        super(hostname, port);
         this.logRequests = false;
         this.webRoot = webRoot;
         this.context = context.getApplicationContext();
diff --git a/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java b/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java
index bb90e1af4..8ec1527af 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java
@@ -151,7 +151,7 @@ public class WifiStateChangeService extends Service {
             Intent intent = new Intent(BROADCAST);
             LocalBroadcastManager.getInstance(WifiStateChangeService.this).sendBroadcast(intent);
             WifiStateChangeService.this.stopSelf();
-            FDroidApp.restartLocalRepoServiceIfRunning();
+            FDroidApp.localRepoWifi.restartIfRunning();
         }
     }
 
diff --git a/F-Droid/src/org/fdroid/fdroid/views/LocalRepoActivity.java b/F-Droid/src/org/fdroid/fdroid/views/LocalRepoActivity.java
index 6a5697392..1450fad45 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/LocalRepoActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/LocalRepoActivity.java
@@ -70,7 +70,7 @@ public class LocalRepoActivity extends ActionBarActivity {
     public void onResume() {
         super.onResume();
         resetNetworkInfo();
-        setRepoSwitchChecked(FDroidApp.isLocalRepoServiceRunning());
+        setRepoSwitchChecked(FDroidApp.localRepoWifi.isRunning());
 
         LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange,
                 new IntentFilter(WifiStateChangeService.BROADCAST));
@@ -83,7 +83,7 @@ public class LocalRepoActivity extends ActionBarActivity {
             }).execute();
 
         // start repo by default
-        FDroidApp.startLocalRepoService(LocalRepoActivity.this);
+        FDroidApp.localRepoWifi.start(LocalRepoActivity.this);
         // reset the timer if viewing this Activity again
         if (stopTimer != null)
             stopTimer.cancel();
@@ -93,7 +93,7 @@ public class LocalRepoActivity extends ActionBarActivity {
 
             @Override
             public void run() {
-                FDroidApp.stopLocalRepoService(LocalRepoActivity.this);
+                FDroidApp.localRepoWifi.stop(LocalRepoActivity.this);
             }
         }, 900000); // 15 minutes
     }
@@ -210,9 +210,9 @@ public class LocalRepoActivity extends ActionBarActivity {
             public void onClick(View v) {
                 setRepoSwitchChecked(repoSwitch.isChecked());
                 if (repoSwitch.isChecked()) {
-                    FDroidApp.startLocalRepoService(LocalRepoActivity.this);
+                    FDroidApp.localRepoWifi.start(LocalRepoActivity.this);
                 } else {
-                    FDroidApp.stopLocalRepoService(LocalRepoActivity.this);
+                    FDroidApp.localRepoWifi.stop(LocalRepoActivity.this);
                     stopTimer.cancel(); // disable automatic stop
                 }
             }
diff --git a/F-Droid/src/org/fdroid/fdroid/views/QrWizardWifiNetworkActivity.java b/F-Droid/src/org/fdroid/fdroid/views/QrWizardWifiNetworkActivity.java
index 81cd6d71f..6157e1729 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/QrWizardWifiNetworkActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/QrWizardWifiNetworkActivity.java
@@ -33,7 +33,7 @@ public class QrWizardWifiNetworkActivity extends ActionBarActivity {
 
         wifiManager = (WifiManager) getSystemService(WIFI_SERVICE);
         wifiManager.setWifiEnabled(true);
-        FDroidApp.startLocalRepoService(this);
+        FDroidApp.localRepoWifi.start(this);
 
         setContentView(R.layout.qr_wizard_activity);
         TextView instructions = (TextView) findViewById(R.id.qrWizardInstructions);
diff --git a/F-Droid/src/org/fdroid/fdroid/views/fragments/ThemeableListFragment.java b/F-Droid/src/org/fdroid/fdroid/views/fragments/ThemeableListFragment.java
index 324c2996a..f2bb8c786 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/fragments/ThemeableListFragment.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/fragments/ThemeableListFragment.java
@@ -21,9 +21,18 @@ public abstract class ThemeableListFragment extends ListFragment {
         return 0;
     }
 
-    protected View getHeaderView(LayoutInflater inflater, ViewGroup container) {
+    protected View getHeaderView() {
+        return headerView;
+    }
+
+    private View headerView = null;
+
+    private View getHeaderView(LayoutInflater inflater, ViewGroup container) {
         if (getHeaderLayout() > 0) {
-            return inflater.inflate(getHeaderLayout(), null, false);
+            if (headerView == null) {
+                headerView = inflater.inflate(getHeaderLayout(), null, false);
+            }
+            return headerView;
         } else {
             return null;
         }
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java
index 654b705dc..47429edf4 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java
@@ -37,7 +37,8 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
     private static final String STATE_WIFI_QR = "wifiQr";
     private static final String STATE_BLUETOOTH_DEVICE_LIST = "bluetoothDeviceList";
 
-    private static final int REQUEST_ENABLE_BLUETOOTH = 1;
+    private static final int REQUEST_BLUETOOTH_ENABLE = 1;
+    private static final int REQUEST_BLUETOOTH_DISCOVERABLE = 2;
 
     private static final String TAG = "org.fdroid.fdroid.views.swap.SwapActivity";
 
@@ -109,7 +110,7 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
 
                     showFragment(new StartSwapFragment(), STATE_START_SWAP);
 
-                    if (FDroidApp.isLocalRepoServiceRunning()) {
+                    if (FDroidApp.localRepoWifi.isRunning()) {
                         showSelectApps();
                         showJoinWifi();
                         attemptToShowNfc();
@@ -190,8 +191,8 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
     }
 
     private void ensureLocalRepoRunning() {
-        if (!FDroidApp.isLocalRepoServiceRunning()) {
-            FDroidApp.startLocalRepoService(this);
+        if (!FDroidApp.localRepoWifi.isRunning()) {
+            FDroidApp.localRepoWifi.start(this);
             initLocalRepoTimer(900000); // 15 mins
         }
     }
@@ -207,7 +208,7 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
         shutdownLocalRepoTimer.schedule(new TimerTask() {
             @Override
             public void run() {
-                FDroidApp.stopLocalRepoService(SwapActivity.this);
+                FDroidApp.localRepoWifi.stop(SwapActivity.this);
             }
         }, timeoutMilliseconds);
 
@@ -215,11 +216,11 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
 
     @Override
     public void stopSwapping() {
-        if (FDroidApp.isLocalRepoServiceRunning()) {
+        if (FDroidApp.localRepoWifi.isRunning()) {
             if (shutdownLocalRepoTimer != null) {
                 shutdownLocalRepoTimer.cancel();
             }
-            FDroidApp.stopLocalRepoService(SwapActivity.this);
+            FDroidApp.localRepoWifi.stop(SwapActivity.this);
         }
         finish();
     }
@@ -242,11 +243,11 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
         if (adapter.isEnabled()) {
             Log.d(TAG, "Bluetooth enabled, will pair with device.");
-            startBluetoothServer();
+            ensureBluetoothDiscoverable();
         } else {
             Log.d(TAG, "Bluetooth disabled, asking user to enable it.");
             Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
-            startActivityForResult(enableBtIntent, REQUEST_ENABLE_BLUETOOTH);
+            startActivityForResult(enableBtIntent, REQUEST_BLUETOOTH_ENABLE);
         }
     }
 
@@ -254,22 +255,51 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
     public void onActivityResult(int requestCode, int resultCode, Intent data) {
         super.onActivityResult(requestCode, resultCode, data);
 
-        if (requestCode == REQUEST_ENABLE_BLUETOOTH) {
+        if (requestCode == REQUEST_BLUETOOTH_ENABLE) {
 
             if (resultCode == RESULT_OK) {
-                Log.d(TAG, "User enabled Bluetooth, will pair with device.");
-                startBluetoothServer();
+                Log.d(TAG, "User enabled Bluetooth, will make sure we are discoverable.");
+                ensureBluetoothDiscoverable();
             } else {
                 // Didn't enable bluetooth
                 Log.d(TAG, "User chose not to enable Bluetooth, so doing nothing (i.e. sticking with wifi).");
             }
 
+        } else if (requestCode == REQUEST_BLUETOOTH_DISCOVERABLE) {
+
+            if (resultCode != RESULT_CANCELED) {
+                Log.d(TAG, "User made Bluetooth discoverable, will proceed to start bluetooth server.");
+                startBluetoothServer();
+            } else {
+                Log.d(TAG, "User chose not to make Bluetooth discoverable, so doing nothing (i.e. sticking with wifi).");
+            }
+
+        }
+    }
+
+    private void ensureBluetoothDiscoverable() {
+        Log.d(TAG, "Ensuring Bluetooth is in discoverable mode.");
+        if (BluetoothAdapter.getDefaultAdapter().getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
+
+            // TODO: Listen for BluetoothAdapter.ACTION_SCAN_MODE_CHANGED and respond if discovery
+            // is cancelled prematurely.
+
+            Log.d(TAG, "Not currently in discoverable mode, so prompting user to enable.");
+            Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
+            intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
+            startActivityForResult(intent, REQUEST_BLUETOOTH_DISCOVERABLE);
+        } else {
+            Log.d(TAG, "Bluetooth is already discoverable, so lets start the Bluetooth server.");
+            startBluetoothServer();
         }
     }
 
     private void startBluetoothServer() {
         Log.d(TAG, "Starting bluetooth server.");
-        new BluetoothServer().start();
+        if (!FDroidApp.localRepoProxy.isRunning()) {
+            FDroidApp.localRepoProxy.start(this);
+        }
+        new BluetoothServer(this).start();
         showBluetoothDeviceList();
     }
 
diff --git a/res/layout-v14/simple_list_item_3.xml b/res/layout-v14/simple_list_item_3.xml
new file mode 100644
index 000000000..c1cb020cf
--- /dev/null
+++ b/res/layout-v14/simple_list_item_3.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+  
+          http://www.apache.org/licenses/LICENSE-2.0
+  
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!--
+    This file was modified for F-Droid to include an additional text item beyond
+    simple_list_item_3. Thought we may as well make it as much as possible the
+    same as the original, and so should essentially build on the original one
+    from the Android SDK.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:orientation="vertical"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+>
+    
+	<TextView android:id="@android:id/text1"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+        android:layout_marginTop="8dip"
+		android:textAppearance="?android:attr/textAppearanceListItem"
+	/>
+		
+	<TextView android:id="@android:id/text2"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:textAppearance="?android:attr/textAppearanceSmall"
+	/>
+
+	<TextView android:id="@+id/text3"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:textAppearance="?android:attr/textAppearanceSmall"
+	/>
+
+</LinearLayout>
diff --git a/res/layout/simple_list_item_3.xml b/res/layout/simple_list_item_3.xml
new file mode 100644
index 000000000..fdde261f8
--- /dev/null
+++ b/res/layout/simple_list_item_3.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+  
+          http://www.apache.org/licenses/LICENSE-2.0
+  
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!--
+    This file was modified for F-Droid to include an additional text item beyond
+    simple_list_item_3. Thought we may as well make it as much as possible the
+    same as the original, and so should essentially build on the original one
+    from the Android SDK.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:orientation="vertical"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+>
+    
+	<TextView android:id="@android:id/text1"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+        android:layout_marginTop="8dip"
+		android:textAppearance="?android:attr/textAppearanceLarge"
+	/>
+		
+	<TextView android:id="@android:id/text2"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:textAppearance="?android:attr/textAppearanceSmall"
+	/>
+
+	<TextView android:id="@+id/text3"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:textAppearance="?android:attr/textAppearanceSmall"
+	/>
+
+</LinearLayout>
diff --git a/res/layout/swap_bluetooth_header.xml b/res/layout/swap_bluetooth_header.xml
index ae9788b29..3977d3c86 100644
--- a/res/layout/swap_bluetooth_header.xml
+++ b/res/layout/swap_bluetooth_header.xml
@@ -9,19 +9,43 @@
     android:layout_height="wrap_content">
 
     <TextView
-            android:layout_width="394dp"
+            android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:id="@+id/device_ip_address"
-            tools:text="Your device name:\nPete's Nexus 4"
-            style="@style/SwapTheme.BluetoothDeviceList.Heading"/>
+            android:id="@+id/device_name_prefix"
+            android:text="Your device is"
+            tools:text="Your device is"
+            android:textAlignment="center"
+            style="@style/SwapTheme.BluetoothDeviceList.Heading" android:paddingTop="10dp"
+            android:paddingBottom="10dp" android:textSize="24sp"/>
 
     <TextView
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            tools:text="Select from devices below"
-            style="@style/SwapTheme.Wizard.Text"/>
+            android:id="@+id/device_name"
+            android:text="Pete's Nexus 4"
+            tools:text="Pete's Nexus 4"
+            android:textAlignment="center"
+            style="@style/SwapTheme.BluetoothDeviceList.Heading" android:paddingBottom="10dp"/>
 
-    <ContentLoadingProgressBar
+    <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:id="@+id/device_address"
+            android:text="(01:02:03:ab:cd:ef)"
+            tools:text="(01:02:03:ab:cd:ef)"
+            android:textAlignment="center"
+            style="@style/SwapTheme.BluetoothDeviceList.Heading" android:paddingBottom="20dp" android:textSize="24sp"/>
+
+    <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Select from devices below, or tap here to scan for more devices..."
+            tools:text="Select from devices below, or tap here to scan for more devices..."
+            android:textAlignment="center"
+            style="@style/SwapTheme.BluetoothDeviceList.Text" android:paddingLeft="20dp" android:paddingRight="20dp"
+            android:paddingBottom="10dp"/>
+
+    <android.support.v4.widget.ContentLoadingProgressBar
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:id="@+id/loading_indicator"/>
diff --git a/src/org/fdroid/fdroid/localrepo/LocalRepoProxyService.java b/src/org/fdroid/fdroid/localrepo/LocalRepoProxyService.java
new file mode 100644
index 000000000..7245f9180
--- /dev/null
+++ b/src/org/fdroid/fdroid/localrepo/LocalRepoProxyService.java
@@ -0,0 +1,29 @@
+package org.fdroid.fdroid.localrepo;
+
+/**
+ * Starts the local repo service but bound to 127.0.0.1.
+ * Also, it does not care about whether wifi is connected or not,
+ * and thus doesn't care about Bonjour.
+ */
+public class LocalRepoProxyService extends LocalRepoService {
+
+    @Override
+    protected void onStartNetworkServices() {
+        // Do nothing
+    }
+
+    @Override
+    protected void onStopNetworkServices() {
+        // Do nothing
+    }
+
+    @Override
+    protected boolean useHttps() {
+        return false;
+    }
+
+    @Override
+    protected String getIpAddressToBindTo() {
+        return "127.0.0.1";
+    }
+}
diff --git a/src/org/fdroid/fdroid/localrepo/LocalRepoWifiService.java b/src/org/fdroid/fdroid/localrepo/LocalRepoWifiService.java
new file mode 100644
index 000000000..cc9cafd2a
--- /dev/null
+++ b/src/org/fdroid/fdroid/localrepo/LocalRepoWifiService.java
@@ -0,0 +1,160 @@
+package org.fdroid.fdroid.localrepo;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.AsyncTask;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.Log;
+import org.fdroid.fdroid.FDroidApp;
+import org.fdroid.fdroid.Preferences;
+import org.fdroid.fdroid.net.WifiStateChangeService;
+
+import javax.jmdns.JmDNS;
+import javax.jmdns.ServiceInfo;
+import java.io.IOException;
+import java.util.HashMap;
+
+public class LocalRepoWifiService extends LocalRepoService {
+
+    private static final String TAG = "org.fdroid.fdroid.localrepo.LocalRepoWifiService";
+    private JmDNS jmdns;
+    private ServiceInfo pairService;
+
+    private BroadcastReceiver onWifiChange = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent i) {
+            stopNetworkServices();
+            startNetworkServices();
+        }
+    };
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        Preferences.get().registerLocalRepoBonjourListeners(localRepoBonjourChangeListener);
+        LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange,
+                new IntentFilter(WifiStateChangeService.BROADCAST));
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        Preferences.get().unregisterLocalRepoBonjourListeners(localRepoBonjourChangeListener);
+        LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange);
+    }
+
+    @Override
+    protected void onStartNetworkServices() {
+        if (Preferences.get().isLocalRepoBonjourEnabled())
+            registerMDNSService();
+        Preferences.get().registerLocalRepoHttpsListeners(localRepoHttpsChangeListener);
+    }
+
+    @Override
+    protected void onStopNetworkServices() {
+        Log.d(TAG, "Stopping local repo network services");
+        Preferences.get().unregisterLocalRepoHttpsListeners(localRepoHttpsChangeListener);
+
+        Log.d(TAG, "Unregistering MDNS service...");
+        unregisterMDNSService();
+    }
+
+    @Override
+    protected boolean useHttps() {
+        return Preferences.get().isLocalRepoHttpsEnabled();
+    }
+
+    @Override
+    protected String getIpAddressToBindTo() {
+        return FDroidApp.ipAddressString;
+    }
+
+    private Preferences.ChangeListener localRepoBonjourChangeListener = new Preferences.ChangeListener() {
+        @Override
+        public void onPreferenceChange() {
+            if (localHttpd.isAlive())
+                if (Preferences.get().isLocalRepoBonjourEnabled())
+                    registerMDNSService();
+                else
+                    unregisterMDNSService();
+        }
+    };
+
+    private Preferences.ChangeListener localRepoHttpsChangeListener = new Preferences.ChangeListener() {
+        @Override
+        public void onPreferenceChange() {
+            Log.i("localRepoHttpsChangeListener", "onPreferenceChange");
+            if (localHttpd.isAlive()) {
+                new AsyncTask<Void, Void, Void>() {
+                    @Override
+                    protected Void doInBackground(Void... params) {
+                        stopNetworkServices();
+                        startNetworkServices();
+                        return null;
+                    }
+                }.execute();
+            }
+        }
+    };
+
+    private void registerMDNSService() {
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                /*
+                 * a ServiceInfo can only be registered with a single instance
+                 * of JmDNS, and there is only ever a single LocalHTTPD port to
+                 * advertise anyway.
+                 */
+                if (pairService != null || jmdns != null)
+                    clearCurrentMDNSService();
+                String repoName = Preferences.get().getLocalRepoName();
+                HashMap<String, String> values = new HashMap<String, String>();
+                values.put("path", "/fdroid/repo");
+                values.put("name", repoName);
+                values.put("fingerprint", FDroidApp.repo.fingerprint);
+                String type;
+                if (Preferences.get().isLocalRepoHttpsEnabled()) {
+                    values.put("type", "fdroidrepos");
+                    type = "_https._tcp.local.";
+                } else {
+                    values.put("type", "fdroidrepo");
+                    type = "_http._tcp.local.";
+                }
+                try {
+                    pairService = ServiceInfo.create(type, repoName, FDroidApp.port, 0, 0, values);
+                    jmdns = JmDNS.create();
+                    jmdns.registerService(pairService);
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }).start();
+    }
+
+    private void unregisterMDNSService() {
+        if (localRepoBonjourChangeListener != null) {
+            Preferences.get().unregisterLocalRepoBonjourListeners(localRepoBonjourChangeListener);
+            localRepoBonjourChangeListener = null;
+        }
+        clearCurrentMDNSService();
+    }
+
+    private void clearCurrentMDNSService() {
+        if (jmdns != null) {
+            if (pairService != null) {
+                jmdns.unregisterService(pairService);
+                pairService = null;
+            }
+            jmdns.unregisterAllServices();
+            try {
+                jmdns.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+            jmdns = null;
+        }
+    }
+}
diff --git a/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java b/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
index 4f77b80a1..886b215f6 100644
--- a/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
+++ b/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
@@ -1,7 +1,7 @@
 package org.fdroid.fdroid.net.bluetooth;
 
-import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothSocket;
 
 import java.io.IOException;
 
@@ -9,29 +9,17 @@ public class BluetoothClient {
 
     private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothClient";
 
-    private final BluetoothAdapter adapter;
     private BluetoothDevice device;
 
-    public BluetoothClient() {
-        this.adapter = BluetoothAdapter.getDefaultAdapter();
-    }
-
-    public void pairWithDevice() throws IOException {
-
-        if (adapter.getBondedDevices().size() == 0) {
-            throw new IOException("No paired Bluetooth devices.");
-        }
-        
-        // TODO: Don't just take a random bluetooth device :)
-
-        device = adapter.getBondedDevices().iterator().next();
-        device.createRfcommSocketToServiceRecord(BluetoothConstants.fdroidUuid());
-
+    public BluetoothClient(BluetoothDevice device) {
+        this.device = device;
     }
 
     public BluetoothConnection openConnection() throws IOException {
-        return null;
-        // return new BluetoothConnection();
+        BluetoothSocket socket = device.createRfcommSocketToServiceRecord(BluetoothConstants.fdroidUuid());
+        BluetoothConnection connection = new BluetoothConnection(socket);
+        connection.open();
+        return connection;
     }
 
 }
diff --git a/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java b/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
index 570f5eba0..76fb1c57d 100644
--- a/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
+++ b/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
@@ -3,9 +3,12 @@ package org.fdroid.fdroid.net.bluetooth;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothServerSocket;
 import android.bluetooth.BluetoothSocket;
+import android.content.Context;
 import android.util.Log;
 import org.fdroid.fdroid.Utils;
+import org.fdroid.fdroid.net.HttpDownloader;
 import org.fdroid.fdroid.net.bluetooth.httpish.Request;
+import org.fdroid.fdroid.net.bluetooth.httpish.Response;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -20,9 +23,14 @@ public class BluetoothServer extends Thread {
     private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothServer";
 
     private BluetoothServerSocket serverSocket;
-
     private List<Connection> clients = new ArrayList<Connection>();
 
+    private final Context context;
+
+    public BluetoothServer(Context context) {
+        this.context = context.getApplicationContext();
+    }
+
     public void close() {
 
         for (Connection connection : clients) {
@@ -50,7 +58,7 @@ public class BluetoothServer extends Thread {
             try {
                 BluetoothSocket clientSocket = serverSocket.accept();
                 if (clientSocket != null && !isInterrupted()) {
-                    Connection client = new Connection(clientSocket);
+                    Connection client = new Connection(context, clientSocket);
                     client.start();
                     clients.add(client);
                 } else {
@@ -67,9 +75,12 @@ public class BluetoothServer extends Thread {
     {
 
         private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothServer.Connection";
-        private BluetoothSocket socket;
 
-        public Connection(BluetoothSocket socket) {
+        private final Context context;
+        private final BluetoothSocket socket;
+
+        public Connection(Context context, BluetoothSocket socket) {
+            this.context = context.getApplicationContext();
             this.socket = socket;
         }
 
@@ -81,6 +92,7 @@ public class BluetoothServer extends Thread {
             BluetoothConnection connection;
             try {
                 connection = new BluetoothConnection(socket);
+                connection.open();
             } catch (IOException e) {
                 Log.e(TAG, "Error listening for incoming connections over bluetooth - " + e.getMessage());
                 return;
@@ -91,7 +103,7 @@ public class BluetoothServer extends Thread {
                 try {
                     Log.d(TAG, "Listening for new Bluetooth request from client.");
                     Request incomingRequest = Request.listenForRequest(connection);
-                    handleRequest(incomingRequest);
+                    handleRequest(incomingRequest).send(connection);
                 } catch (IOException e) {
                     Log.e(TAG, "Error receiving incoming connection over bluetooth - " + e.getMessage());
                 }
@@ -103,10 +115,31 @@ public class BluetoothServer extends Thread {
 
         }
 
-        private void handleRequest(Request request) {
+        private Response handleRequest(Request request) throws IOException {
 
             Log.d(TAG, "Received Bluetooth request from client, will process it now.");
 
+            try {
+                HttpDownloader downloader = new HttpDownloader("http://127.0.0.1/" + request.getPath(), context);
+
+                Response.ResponseBuilder builder;
+
+                if (request.getMethod().equals(Request.Methods.HEAD)) {
+                    builder = new Response.ResponseBuilder();
+                } else {
+                    builder = new Response.ResponseBuilder(downloader.getInputStream());
+                }
+
+                // TODO: At this stage, will need to download the file to get this info.
+                // However, should be able to make totalDownloadSize and getCacheTag work without downloading.
+                return builder
+                        .setStatusCode(200)
+                        .build();
+
+            } catch (IOException e) {
+                throw new IOException("Error getting file " + request.getPath() + " from local repo proxy - " + e.getMessage(), e);
+            }
+
         }
     }
 }
diff --git a/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java b/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java
index 3066c7142..be10cd4e3 100644
--- a/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java
+++ b/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java
@@ -1,8 +1,13 @@
 package org.fdroid.fdroid.net.bluetooth.httpish;
 
+import android.util.Log;
 import org.fdroid.fdroid.net.bluetooth.BluetoothConnection;
 
-import java.io.*;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
@@ -10,6 +15,8 @@ import java.util.Map;
 public class Request {
 
 
+    private static final String TAG = "org.fdroid.fdroid.net.bluetooth.httpish.Request";
+
     public static interface Methods {
         public static final String HEAD = "HEAD";
         public static final String GET  = "GET";
@@ -27,6 +34,9 @@ public class Request {
         this.method = method;
         this.path = path;
         this.connection = connection;
+
+        output = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream()));
+        input = new BufferedReader(new InputStreamReader(connection.getInputStream()));
     }
 
     public static Request createHEAD(String path, BluetoothConnection connection)
@@ -40,8 +50,7 @@ public class Request {
 
     public Response send() throws IOException {
 
-        output = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream()));
-        input  = new BufferedReader(new InputStreamReader(connection.getInputStream()));
+        Log.d(TAG, "Sending request to server (" + path + ")");
 
         output.write(method);
         output.write(' ');
@@ -49,12 +58,23 @@ public class Request {
 
         output.write("\n\n");
 
+        output.flush();
+
+        Log.d(TAG, "Finished sending request, now attempting to read response status code...");
+
         int responseCode = readResponseCode();
+
+        Log.d(TAG, "Read response code " + responseCode + " from server, now reading headers...");
+
         Map<String, String> headers = readHeaders();
 
+        Log.d(TAG, "Read " + headers.size() + " headers");
+
         if (method.equals(Methods.HEAD)) {
+            Log.d(TAG, "Request was a " + Methods.HEAD + " request, not including anything other than headers and status...");
             return new Response(responseCode, headers);
         } else {
+            Log.d(TAG, "Request was a " + Methods.GET + " request, so including content stream in response...");
             return new Response(responseCode, headers, connection.getInputStream());
         }
 
@@ -109,9 +129,9 @@ public class Request {
 
         // TODO: Error handling
         int firstSpace = line.indexOf(' ');
-        int secondSpace = line.indexOf(' ', firstSpace);
+        int secondSpace = line.indexOf(' ', firstSpace + 1);
 
-        String status = line.substring(firstSpace, secondSpace);
+        String status = line.substring(firstSpace + 1, secondSpace);
         return Integer.parseInt(status);
     }
 
@@ -135,6 +155,12 @@ public class Request {
         return headers;
     }
 
+    public String getPath() {
+        return path;
+    }
 
+    public String getMethod() {
+        return method;
+    }
 
 }
diff --git a/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java b/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java
index fcf55eb28..44c8c900e 100644
--- a/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java
+++ b/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java
@@ -1,13 +1,22 @@
 package org.fdroid.fdroid.net.bluetooth.httpish;
 
+import android.util.Log;
+import org.fdroid.fdroid.Utils;
+import org.fdroid.fdroid.net.bluetooth.BluetoothConnection;
 import org.fdroid.fdroid.net.bluetooth.FileDetails;
 import org.fdroid.fdroid.net.bluetooth.httpish.headers.Header;
 
+import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.HashMap;
 import java.util.Map;
 
 public class Response {
 
+    private static final String TAG = "org.fdroid.fdroid.net.bluetooth.httpish.Response";
+
     private int statusCode;
     private Map<String, String> headers;
     private final InputStream contentStream;
@@ -44,13 +53,80 @@ public class Response {
         return details;
     }
 
-    /**
-     * After parsing a response,
-     */
     public InputStream toContentStream() throws UnsupportedOperationException {
         if (contentStream == null) {
             throw new UnsupportedOperationException("This kind of response doesn't have a content stream. Did you perform a HEAD request instead of a GET request?");
         }
         return contentStream;
     }
+
+    public void send(BluetoothConnection connection) throws IOException {
+
+        Log.d(TAG, "Sending Bluetooth HTTP-ish response...");
+
+        Writer output = new OutputStreamWriter(connection.getOutputStream());
+        output.write("HTTP(ish)/0.1 200 OK\n");
+
+        for (Map.Entry<String, String> entry : headers.entrySet()) {
+            output.write(entry.getKey());
+            output.write(": ");
+            output.write(entry.getValue());
+            output.write("\n");
+        }
+
+        output.write("\n");
+        output.flush();
+
+        if (contentStream != null) {
+            Utils.copy(contentStream, connection.getOutputStream());
+        }
+
+        output.flush();
+
+    }
+
+    public static class ResponseBuilder {
+
+        private InputStream contentStream;
+        private int statusCode = 200;
+        private int fileSize = -1;
+        private String etag = null;
+
+        public ResponseBuilder() {}
+
+        public ResponseBuilder(InputStream contentStream) {
+            this.contentStream = contentStream;
+        }
+
+        public ResponseBuilder setStatusCode(int statusCode) {
+            this.statusCode = statusCode;
+            return this;
+        }
+
+        public ResponseBuilder setFileSize(int fileSize) {
+            this.fileSize = fileSize;
+            return this;
+        }
+
+        public ResponseBuilder setETag(String etag) {
+            this.etag = etag;
+            return this;
+        }
+
+        public Response build() {
+
+            Map<String, String> headers = new HashMap<>(3);
+
+            if (fileSize > 0) {
+                headers.put("Content-Length", Integer.toString(fileSize));
+            }
+
+            if (etag != null) {
+                headers.put( "ETag", etag);
+            }
+
+            return new Response(statusCode, headers, contentStream);
+        }
+
+    }
 }
diff --git a/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java b/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java
index f9e68699b..2467aea31 100644
--- a/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java
+++ b/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java
@@ -1,26 +1,168 @@
 package org.fdroid.fdroid.views.swap;
 
+import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
+import android.content.BroadcastReceiver;
 import android.content.Context;
-import android.database.Cursor;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.os.Bundle;
+import android.support.v4.widget.ContentLoadingProgressBar;
+import android.util.Log;
 import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ArrayAdapter;
 import android.widget.ListView;
 import android.widget.TextView;
-import org.fdroid.fdroid.FDroidApp;
 import org.fdroid.fdroid.R;
-import org.fdroid.fdroid.data.InstalledAppProvider;
+import org.fdroid.fdroid.net.bluetooth.BluetoothClient;
+import org.fdroid.fdroid.net.bluetooth.BluetoothConnection;
+import org.fdroid.fdroid.net.bluetooth.httpish.Request;
+import org.fdroid.fdroid.net.bluetooth.httpish.Response;
 import org.fdroid.fdroid.views.fragments.ThemeableListFragment;
 
+import java.io.IOException;
 import java.util.List;
 
 public class BluetoothDeviceListFragment extends ThemeableListFragment {
 
+    private static final String TAG = "org.fdroid.fdroid.views.swap.BluetoothDeviceListFragment";
+
     private Adapter adapter = null;
 
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setHasOptionsMenu(false);
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        setEmptyText("No bluetooth devices found. Is the other device \"discoverable\"?");
+
+        adapter = new Adapter(
+            new ContextThemeWrapper(getActivity(), R.style.SwapTheme_BluetoothDeviceList_ListItem),
+            R.layout.select_local_apps_list_item
+        );
+
+        populateDeviceList();
+
+        setListAdapter(adapter);
+        setListShown(false); // start out with a progress indicator
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        View view = super.onCreateView(inflater, container, savedInstanceState);
+        View headerView = getHeaderView();
+        if (headerView == null) {
+            Log.e(TAG, "Could not find header view, although we expected one to exist.");
+        } else {
+            headerView.setOnTouchListener(new View.OnTouchListener() {
+                @Override
+                public boolean onTouch(View v, MotionEvent event) {
+                    initiateBluetoothScan();
+                    return true;
+                }
+            });
+        }
+        return view;
+    }
+
+    private void initiateBluetoothScan()
+    {
+        BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
+        BroadcastReceiver deviceFoundReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (BluetoothDevice.ACTION_FOUND.equals(intent.getAction())) {
+                    BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+                    boolean exists = false;
+                    for (int i = 0; i < adapter.getCount(); i ++) {
+                        if (adapter.getItem(i).getAddress().equals(device.getAddress())) {
+                            exists = true;
+                            break;
+                        }
+                    }
+
+                    if (!exists) {
+                        adapter.add(device);
+                    }
+                }
+            }
+        };
+
+        ((ContentLoadingProgressBar)getView().findViewById(R.id.loading_indicator)).show();
+        getActivity().registerReceiver(deviceFoundReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND));
+
+        if (!bluetooth.startDiscovery()) {
+            // TODO: Discovery did not start for some reason :(
+            Log.e(TAG, "Could not start bluetooth discovery, but am not sure why :(");
+        }
+    }
+
+    private void populateDeviceList()
+    {
+        for (BluetoothDevice device : BluetoothAdapter.getDefaultAdapter().getBondedDevices()) {
+            adapter.add(device);
+        }
+    }
+
+    @Override
+    public void onListItemClick(ListView l, View v, int position, long id) {
+
+        // "position" includes the header view, so ignore that.
+        if (position == 0) {
+            return;
+        }
+
+        BluetoothDevice device = adapter.getItem(position - 1);
+
+        // TODO: I think that I can connect regardless of the bond state.
+        // It sounds like when I attempt to connect to a non-bonded peer, then
+        // Android initiates the pairing dialog on our behalf.
+
+        BluetoothClient client = new BluetoothClient(device);
+
+        try {
+            Log.d(TAG, "Testing bluetooth connection (opening connection first).");
+            BluetoothConnection connection = client.openConnection();
+            Log.d(TAG, "Creating HEAD request for resource at \"/\"...");
+            Request head = Request.createHEAD("/", connection);
+            Log.d(TAG, "Sending request...");
+            Response response = head.send();
+            Log.d(TAG, "Response from bluetooth: " + response.getStatusCode());
+        } catch (IOException e) {
+
+        }
+
+        /*if (device.getBondState() == BluetoothDevice.BOND_NONE) {
+            // attempt to bond
+
+        } else if (device.getBondState() == BluetoothDevice.BOND_BONDING) {
+            // wait for bonding to finish
+
+        } else if (device.getBondState() == BluetoothDevice.BOND_BONDED) {
+            // connect
+            BluetoothClient client = new BluetoothClient(device);
+        }*/
+    }
+
+    @Override
+    protected int getThemeStyle() {
+        return R.style.SwapTheme_BluetoothDeviceList;
+    }
+
+    @Override
+    protected int getHeaderLayout() {
+        return R.layout.swap_bluetooth_header;
+    }
+
     private class Adapter extends ArrayAdapter<BluetoothDevice> {
 
         public Adapter(Context context, int resource) {
@@ -51,61 +193,37 @@ public class BluetoothDeviceListFragment extends ThemeableListFragment {
         public View getView(int position, View convertView, ViewGroup parent) {
             View view;
             if (convertView == null) {
-                view = getActivity().getLayoutInflater().inflate(android.R.layout.simple_list_item_2, parent);
+                view = getActivity().getLayoutInflater().inflate(R.layout.simple_list_item_3, null);
             } else {
                 view = convertView;
             }
 
             BluetoothDevice device = getItem(position);
             TextView nameView = (TextView)view.findViewById(android.R.id.text1);
-            TextView descriptionView = (TextView)view.findViewById(android.R.id.text2);
+            TextView addressView = (TextView)view.findViewById(android.R.id.text2);
+            TextView descriptionView = (TextView)view.findViewById(R.id.text3);
 
             nameView.setText(device.getName());
-            descriptionView.setText(device.getAddress());
+            addressView.setText(device.getAddress());
+            descriptionView.setText(bondStateToLabel(device.getBondState()));
 
             return view;
         }
-    }
 
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setHasOptionsMenu(false);
-    }
-
-    @Override
-    public void onActivityCreated(Bundle savedInstanceState) {
-        super.onActivityCreated(savedInstanceState);
-
-        setEmptyText("No bluetooth devices found. Is the other device \"discoverable\"?");
-
-        Adapter adapter = new Adapter(
-            new ContextThemeWrapper(getActivity(), R.style.SwapTheme_BluetoothDeviceList_ListItem),
-            R.layout.select_local_apps_list_item
-        );
-
-        setListAdapter(adapter);
-        setListShown(false); // start out with a progress indicator
-    }
-
-    @Override
-    public void onListItemClick(ListView l, View v, int position, long id) {
-        Cursor c = (Cursor) l.getAdapter().getItem(position);
-        String packageName = c.getString(c.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID));
-        if (FDroidApp.selectedApps.contains(packageName)) {
-            FDroidApp.selectedApps.remove(packageName);
-        } else {
-            FDroidApp.selectedApps.add(packageName);
+        private String bondStateToLabel(int deviceBondState)
+        {
+            if (deviceBondState == BluetoothDevice.BOND_BONDED) {
+                // TODO: Is the term "Bonded device" common parlance among phone users?
+                // It sounds a bit technical to me, maybe something more lay like "Previously connected".
+                // Although it is technically not as accurate, it would make sense to more people...
+                return "Bonded";
+            } else if (deviceBondState == BluetoothDevice.BOND_BONDING) {
+                return "Currently bonding...";
+            } else {
+                // TODO: Might be a little bit harsh, makes it sound more malicious than it should.
+                return "Unknown device";
+            }
         }
     }
 
-    @Override
-    protected int getThemeStyle() {
-        return R.style.SwapTheme_StartSwap;
-    }
-
-    @Override
-    protected int getHeaderLayout() {
-        return R.layout.swap_bluetooth_header;
-    }
 }