From 45a3efa2b37208be416ce7866f36a99a493ec539 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@ivt.com.au>
Date: Sat, 12 Apr 2014 08:09:14 +0000
Subject: [PATCH 01/16] WIP: Started to implement the general concept of
 BluetoothDownloader

I'm not 100% sure it is the right architecture yet, there wil no doubt be
things that crop up as I continue to implement it. However it seems to
be alright to work with so far.
---
 .../fdroid/net/bluetooth/BluetoothClient.java | 81 ++++++++++++++++
 .../net/bluetooth/BluetoothConstants.java     | 14 +++
 .../net/bluetooth/BluetoothDownloader.java    | 94 +++++++++++++++++++
 .../fdroid/net/bluetooth/FileDetails.java     | 23 +++++
 .../UnexpectedResponseException.java          | 12 +++
 .../fdroid/net/bluetooth/httpish/Request.java | 90 ++++++++++++++++++
 .../net/bluetooth/httpish/Response.java       | 56 +++++++++++
 .../httpish/headers/ContentLengthHeader.java  | 16 ++++
 .../bluetooth/httpish/headers/ETagHeader.java | 16 ++++
 .../net/bluetooth/httpish/headers/Header.java | 25 +++++
 10 files changed, 427 insertions(+)
 create mode 100644 src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
 create mode 100644 src/org/fdroid/fdroid/net/bluetooth/BluetoothConstants.java
 create mode 100644 src/org/fdroid/fdroid/net/bluetooth/BluetoothDownloader.java
 create mode 100644 src/org/fdroid/fdroid/net/bluetooth/FileDetails.java
 create mode 100644 src/org/fdroid/fdroid/net/bluetooth/UnexpectedResponseException.java
 create mode 100644 src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java
 create mode 100644 src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java
 create mode 100644 src/org/fdroid/fdroid/net/bluetooth/httpish/headers/ContentLengthHeader.java
 create mode 100644 src/org/fdroid/fdroid/net/bluetooth/httpish/headers/ETagHeader.java
 create mode 100644 src/org/fdroid/fdroid/net/bluetooth/httpish/headers/Header.java

diff --git a/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java b/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
new file mode 100644
index 000000000..68ddf52e6
--- /dev/null
+++ b/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
@@ -0,0 +1,81 @@
+package org.fdroid.fdroid.net.bluetooth;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothSocket;
+import android.util.Log;
+import org.fdroid.fdroid.Utils;
+
+import java.io.*;
+import java.util.UUID;
+
+public class BluetoothClient {
+
+    private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothClient";
+
+    private BluetoothAdapter adapter;
+    private BluetoothDevice device;
+
+    public BluetoothClient(BluetoothAdapter adapter) {
+        this.adapter = adapter;
+    }
+
+    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();
+
+    }
+
+    public Connection openConnection() throws IOException {
+        return new Connection();
+    }
+
+    public class Connection {
+
+        private InputStream input = null;
+        private OutputStream output = null;
+
+        private BluetoothSocket socket;
+
+        private Connection() throws IOException {
+            Log.d(TAG, "Attempting to create connection to Bluetooth device '" + device.getName() + "'...");
+            socket = device.createRfcommSocketToServiceRecord(UUID.fromString(BluetoothConstants.fdroidUuid()));
+        }
+
+        public InputStream getInputStream() {
+            return input;
+        }
+
+        public OutputStream getOutputStream() {
+            return output;
+        }
+
+        public void open() throws IOException {
+            socket.connect();
+            input  = socket.getInputStream();
+            output = socket.getOutputStream();
+            Log.d(TAG, "Opened connection to Bluetooth device '" + device.getName() + "'");
+        }
+
+        public void closeQuietly() {
+            Utils.closeQuietly(input);
+            Utils.closeQuietly(output);
+            Utils.closeQuietly(socket);
+        }
+
+        public void close() throws IOException {
+            if (input == null || output == null) {
+                throw new RuntimeException("Cannot close() a BluetoothConnection before calling open()" );
+            }
+
+            input.close();
+            output.close();
+            socket.close();
+        }
+    }
+}
diff --git a/src/org/fdroid/fdroid/net/bluetooth/BluetoothConstants.java b/src/org/fdroid/fdroid/net/bluetooth/BluetoothConstants.java
new file mode 100644
index 000000000..7a44a480c
--- /dev/null
+++ b/src/org/fdroid/fdroid/net/bluetooth/BluetoothConstants.java
@@ -0,0 +1,14 @@
+package org.fdroid.fdroid.net.bluetooth;
+
+/**
+ * We need some shared information between the client and the server app.
+ */
+public class BluetoothConstants {
+
+    public static String fdroidUuid() {
+        // TODO: Generate a UUID deterministically from, e.g. "org.fdroid.fdroid.net.Bluetooth";
+        // This UUID is just from the first example at http://www.ietf.org/rfc/rfc4122.txt
+        return "f81d4fae-7dec-11d0-a765-00a0c91e6bf6";
+    }
+
+}
diff --git a/src/org/fdroid/fdroid/net/bluetooth/BluetoothDownloader.java b/src/org/fdroid/fdroid/net/bluetooth/BluetoothDownloader.java
new file mode 100644
index 000000000..da66f67d6
--- /dev/null
+++ b/src/org/fdroid/fdroid/net/bluetooth/BluetoothDownloader.java
@@ -0,0 +1,94 @@
+package org.fdroid.fdroid.net.bluetooth;
+
+import android.content.Context;
+import android.util.Log;
+import org.fdroid.fdroid.net.Downloader;
+import org.fdroid.fdroid.net.bluetooth.httpish.Request;
+import org.fdroid.fdroid.net.bluetooth.httpish.Response;
+
+import java.io.*;
+import java.net.MalformedURLException;
+
+public class BluetoothDownloader extends Downloader {
+
+    private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothDownloader";
+
+    private BluetoothClient client;
+    private FileDetails fileDetails;
+
+    public BluetoothDownloader(BluetoothClient client, String destFile, Context ctx) throws FileNotFoundException, MalformedURLException {
+        super(destFile, ctx);
+        this.client = client;
+    }
+
+    public BluetoothDownloader(BluetoothClient client, Context ctx) throws IOException {
+        super(ctx);
+        this.client = client;
+    }
+
+    public BluetoothDownloader(BluetoothClient client, File destFile) throws FileNotFoundException, MalformedURLException {
+        super(destFile);
+        this.client = client;
+    }
+
+    public BluetoothDownloader(BluetoothClient client, File destFile, Context ctx) throws IOException {
+        super(destFile, ctx);
+        this.client = client;
+    }
+
+    public BluetoothDownloader(BluetoothClient client, OutputStream output) throws MalformedURLException {
+        super(output);
+        this.client = client;
+    }
+
+    @Override
+    public InputStream inputStream() throws IOException {
+        Response response = new Request(Request.Methods.GET, client).send();
+        fileDetails = response.toFileDetails();
+        return response.toContentStream();
+    }
+
+    /**
+     * May return null if an error occurred while getting file details.
+     * TODO: Should we throw an exception? Everywhere else in this blue package throws IO exceptions weely neely.
+     * Will probably require some thought as to how the API looks, with regards to all of the public methods
+     * and their signatures.
+     */
+    public FileDetails getFileDetails() {
+        if (fileDetails == null) {
+            Log.d(TAG, "Going to Bluetooth \"server\" to get file details.");
+            try {
+                fileDetails = new Request(Request.Methods.HEAD, client).send().toFileDetails();
+            } catch (IOException e) {
+                Log.e(TAG, "Error getting file details from Bluetooth \"server\": " + e.getMessage());
+            }
+        }
+        return fileDetails;
+    }
+
+    @Override
+    public boolean hasChanged() {
+        return getFileDetails().getCacheTag().equals(getCacheTag());
+    }
+
+    @Override
+    public int totalDownloadSize() {
+        return getFileDetails().getFileSize();
+    }
+
+    @Override
+    public void download() throws IOException, InterruptedException {
+        downloadFromStream();
+    }
+
+    @Override
+    public boolean isCached() {
+        FileDetails details = getFileDetails();
+        return (
+            details != null &&
+            details.getCacheTag() != null &&
+            details.getCacheTag().equals(getCacheTag())
+        );
+    }
+
+}
diff --git a/src/org/fdroid/fdroid/net/bluetooth/FileDetails.java b/src/org/fdroid/fdroid/net/bluetooth/FileDetails.java
new file mode 100644
index 000000000..f7148a91f
--- /dev/null
+++ b/src/org/fdroid/fdroid/net/bluetooth/FileDetails.java
@@ -0,0 +1,23 @@
+package org.fdroid.fdroid.net.bluetooth;
+
+public class FileDetails {
+
+    private String cacheTag;
+    private int fileSize;
+
+    public String getCacheTag() {
+        return cacheTag;
+    }
+
+    public int getFileSize() {
+        return fileSize;
+    }
+
+    public void setFileSize(int fileSize) {
+        this.fileSize = fileSize;
+    }
+
+    public void setCacheTag(String cacheTag) {
+        this.cacheTag = cacheTag;
+    }
+}
diff --git a/src/org/fdroid/fdroid/net/bluetooth/UnexpectedResponseException.java b/src/org/fdroid/fdroid/net/bluetooth/UnexpectedResponseException.java
new file mode 100644
index 000000000..518b03dbd
--- /dev/null
+++ b/src/org/fdroid/fdroid/net/bluetooth/UnexpectedResponseException.java
@@ -0,0 +1,12 @@
+package org.fdroid.fdroid.net.bluetooth;
+
+public class UnexpectedResponseException extends Exception {
+
+    public UnexpectedResponseException(String message) {
+        super(message);
+    }
+
+    public UnexpectedResponseException(String message, Throwable cause) {
+        super("Unexpected response from Bluetooth server: '" + message + "'", cause);
+    }
+}
\ No newline at end of file
diff --git a/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java b/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java
new file mode 100644
index 000000000..32ae07eef
--- /dev/null
+++ b/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java
@@ -0,0 +1,90 @@
+package org.fdroid.fdroid.net.bluetooth.httpish;
+
+import org.fdroid.fdroid.net.bluetooth.BluetoothClient;
+
+import java.io.*;
+import java.util.HashMap;
+import java.util.Map;
+
+public class Request {
+
+    public static interface Methods {
+        public static final String HEAD = "HEAD";
+        public static final String GET  = "GET";
+    }
+
+    private final BluetoothClient client;
+    private final String method;
+
+    private BluetoothClient.Connection connection;
+    private BufferedWriter output;
+    private BufferedReader input;
+
+    public Request(String method, BluetoothClient client) {
+        this.method = method;
+        this.client = client;
+    }
+
+    public Response send() throws IOException {
+
+        connection = client.openConnection();
+        output = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream()));
+        input  = new BufferedReader(new InputStreamReader(connection.getInputStream()));
+
+        output.write(method);
+
+        int responseCode = readResponseCode();
+        Map<String, String> headers = readHeaders();
+
+        if (method.equals(Methods.HEAD)) {
+            return new Response(responseCode, headers);
+        } else {
+            return new Response(responseCode, headers, connection.getInputStream());
+        }
+
+    }
+
+    /**
+     * First line of a HTTP response is the status line:
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1
+     * The first part is the HTTP version, followed by a space, then the status code, then
+     * a space, and then the status label (which may contain spaces).
+     */
+    private int readResponseCode() throws IOException {
+        String line = input.readLine();
+        if (line == null) {
+            // TODO: What to do?
+            return -1;
+        }
+
+        // TODO: Error handling
+        int firstSpace = line.indexOf(' ');
+        int secondSpace = line.indexOf(' ', firstSpace);
+
+        String status = line.substring(firstSpace, secondSpace);
+        return Integer.parseInt(status);
+    }
+
+    /**
+     * Subsequent lines (after the status line) represent the headers, which are case
+     * insensitive and may be multi-line. We don't deal with multi-line headers in
+     * our HTTP-ish implementation.
+     */
+    private Map<String, String> readHeaders() throws IOException {
+        Map<String, String> headers = new HashMap<String, String>();
+        String responseLine = input.readLine();
+        while (responseLine != null && responseLine.length() > 0) {
+
+            // TODO: Error handling
+            String[] parts = responseLine.split(":");
+            String header = parts[0].trim();
+            String value  = parts[1].trim();
+            headers.put(header, value);
+            responseLine = input.readLine();
+        }
+        return headers;
+    }
+
+
+
+}
diff --git a/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java b/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java
new file mode 100644
index 000000000..fcf55eb28
--- /dev/null
+++ b/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java
@@ -0,0 +1,56 @@
+package org.fdroid.fdroid.net.bluetooth.httpish;
+
+import org.fdroid.fdroid.net.bluetooth.FileDetails;
+import org.fdroid.fdroid.net.bluetooth.httpish.headers.Header;
+
+import java.io.InputStream;
+import java.util.Map;
+
+public class Response {
+
+    private int statusCode;
+    private Map<String, String> headers;
+    private final InputStream contentStream;
+
+    public Response(int statusCode, Map<String, String> headers) {
+        this(statusCode, headers, null);
+    }
+
+    /**
+     * This class expects 'contentStream' to be open, and ready for use.
+     * It will not close it either. However it will block wile doing things
+     * so you can call a method, wait for it to finish, and then close
+     * it afterwards if you like.
+     */
+    public Response(int statusCode, Map<String, String> headers, InputStream contentStream) {
+        this.statusCode = statusCode;
+        this.headers = headers;
+        this.contentStream = contentStream;
+    }
+
+    public int getStatusCode() {
+        return statusCode;
+    }
+
+    /**
+     * Extracts meaningful headers from the response into a more useful and safe
+     * {@link org.fdroid.fdroid.net.bluetooth.FileDetails} object.
+     */
+    public FileDetails toFileDetails() {
+        FileDetails details = new FileDetails();
+        for (Map.Entry<String, String> entry : headers.entrySet()) {
+            Header.process(details, entry.getKey(), entry.getValue());
+        }
+        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;
+    }
+}
diff --git a/src/org/fdroid/fdroid/net/bluetooth/httpish/headers/ContentLengthHeader.java b/src/org/fdroid/fdroid/net/bluetooth/httpish/headers/ContentLengthHeader.java
new file mode 100644
index 000000000..a2cc07c6c
--- /dev/null
+++ b/src/org/fdroid/fdroid/net/bluetooth/httpish/headers/ContentLengthHeader.java
@@ -0,0 +1,16 @@
+package org.fdroid.fdroid.net.bluetooth.httpish.headers;
+
+import org.fdroid.fdroid.net.bluetooth.FileDetails;
+
+public class ContentLengthHeader extends Header {
+
+    @Override
+    public String getName() {
+        return "content-length";
+    }
+
+    public void handle(FileDetails details, String value) {
+        details.setFileSize(Integer.parseInt(value));
+    }
+
+}
\ No newline at end of file
diff --git a/src/org/fdroid/fdroid/net/bluetooth/httpish/headers/ETagHeader.java b/src/org/fdroid/fdroid/net/bluetooth/httpish/headers/ETagHeader.java
new file mode 100644
index 000000000..81eb41dc3
--- /dev/null
+++ b/src/org/fdroid/fdroid/net/bluetooth/httpish/headers/ETagHeader.java
@@ -0,0 +1,16 @@
+package org.fdroid.fdroid.net.bluetooth.httpish.headers;
+
+import org.fdroid.fdroid.net.bluetooth.FileDetails;
+
+public class ETagHeader extends Header {
+
+    @Override
+    public String getName() {
+        return "etag";
+    }
+
+    public void handle(FileDetails details, String value) {
+        details.setCacheTag(value);
+    }
+
+}
diff --git a/src/org/fdroid/fdroid/net/bluetooth/httpish/headers/Header.java b/src/org/fdroid/fdroid/net/bluetooth/httpish/headers/Header.java
new file mode 100644
index 000000000..30327021c
--- /dev/null
+++ b/src/org/fdroid/fdroid/net/bluetooth/httpish/headers/Header.java
@@ -0,0 +1,25 @@
+package org.fdroid.fdroid.net.bluetooth.httpish.headers;
+
+import org.fdroid.fdroid.net.bluetooth.FileDetails;
+
+public abstract class Header {
+
+    private static Header[] VALID_HEADERS = {
+        new ContentLengthHeader(),
+        new ETagHeader(),
+    };
+
+    protected abstract String getName();
+    protected abstract void handle(FileDetails details, String value);
+
+    public static void process(FileDetails details, String header, String value) {
+        header = header.toLowerCase();
+        for (Header potentialHeader : VALID_HEADERS) {
+            if (potentialHeader.getName().equals(header)) {
+                potentialHeader.handle(details, value);
+                break;
+            }
+        }
+    }
+
+}

From fba02e32b5ee883567ffecc2a692310996458a60 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@ivt.com.au>
Date: Wed, 8 Oct 2014 07:41:04 +1030
Subject: [PATCH 02/16] WIP: Bluetooth list of paired devices.

---
 F-Droid/AndroidManifest.xml                   |   1 +
 F-Droid/res/layout/swap_join_wifi.xml         |   2 +-
 F-Droid/res/values/styles.xml                 |  11 ++
 .../fdroid/views/swap/JoinWifiFragment.java   |  10 ++
 .../fdroid/views/swap/SwapActivity.java       |  66 ++++++++++-
 .../fdroid/views/swap/SwapProcessManager.java |   1 +
 bluetooth-notes.txt                           |  21 ++++
 res/layout/swap_bluetooth_header.xml          |  29 +++++
 .../{bluetooth => }/BluetoothDownloader.java  |  36 +++---
 .../fdroid/net/bluetooth/BluetoothClient.java |  62 ++--------
 .../net/bluetooth/BluetoothConnection.java    |  61 ++++++++++
 .../net/bluetooth/BluetoothConstants.java     |   6 +-
 .../fdroid/net/bluetooth/BluetoothServer.java | 112 ++++++++++++++++++
 .../fdroid/net/bluetooth/httpish/Request.java |  64 ++++++++--
 .../swap/BluetoothDeviceListFragment.java     | 111 +++++++++++++++++
 15 files changed, 506 insertions(+), 87 deletions(-)
 create mode 100644 bluetooth-notes.txt
 create mode 100644 res/layout/swap_bluetooth_header.xml
 rename src/org/fdroid/fdroid/net/{bluetooth => }/BluetoothDownloader.java (61%)
 create mode 100644 src/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java
 create mode 100644 src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
 create mode 100644 src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java

diff --git a/F-Droid/AndroidManifest.xml b/F-Droid/AndroidManifest.xml
index 2cbd97796..42b92faf2 100644
--- a/F-Droid/AndroidManifest.xml
+++ b/F-Droid/AndroidManifest.xml
@@ -44,6 +44,7 @@
     <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
     <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
     <uses-permission android:name="android.permission.BLUETOOTH" />
+    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
         android:maxSdkVersion="18" />
diff --git a/F-Droid/res/layout/swap_join_wifi.xml b/F-Droid/res/layout/swap_join_wifi.xml
index b59f544d1..bd778bfb5 100644
--- a/F-Droid/res/layout/swap_join_wifi.xml
+++ b/F-Droid/res/layout/swap_join_wifi.xml
@@ -25,12 +25,12 @@
             android:layout_below="@+id/text_description"
             android:layout_centerHorizontal="true" />
 
-    <!--
     <Button style="@style/SwapTheme.Wizard.OptionButton"
             android:id="@+id/btn_bluetooth"
             android:text="@string/swap_use_bluetooth"
             android:layout_alignParentBottom="true" />
 
+    <!--
     <Button style="@style/SwapTheme.Wizard.OptionButton"
             android:text="@string/swap_wifi_help"
             android:layout_above="@id/btn_bluetooth"
diff --git a/F-Droid/res/values/styles.xml b/F-Droid/res/values/styles.xml
index a0522db1a..9626b410e 100644
--- a/F-Droid/res/values/styles.xml
+++ b/F-Droid/res/values/styles.xml
@@ -43,6 +43,17 @@
         <item name="android:background">@color/white</item>
     </style>
 
+    <style name="SwapTheme.BluetoothDeviceList" parent="AppThemeLightWithDarkActionBar">
+    </style>
+
+    <style name="SwapTheme.BluetoothDeviceList.ListItem" parent="AppThemeLightWithDarkActionBar">
+    </style>
+
+    <style name="SwapTheme.BluetoothDeviceList.Heading" parent="@style/SwapTheme.Wizard.MainText">
+        <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">
     </style>
 
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/JoinWifiFragment.java b/F-Droid/src/org/fdroid/fdroid/views/swap/JoinWifiFragment.java
index 9723945b9..800c05eef 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/JoinWifiFragment.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/JoinWifiFragment.java
@@ -18,6 +18,7 @@ import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageView;
+import android.widget.Button;
 import android.widget.TextView;
 
 import org.fdroid.fdroid.FDroidApp;
@@ -56,6 +57,15 @@ public class JoinWifiFragment extends Fragment {
                 openAvailableNetworks();
             }
         });
+
+        Button bluetooth = (Button)joinWifiView.findViewById(R.id.btn_bluetooth);
+        bluetooth.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                ((SwapProcessManager)getActivity()).connectWithBluetooth();
+            }
+        });
+
         return joinWifiView;
     }
 
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 2c556bf14..654b705dc 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java
@@ -1,7 +1,9 @@
 package org.fdroid.fdroid.views.swap;
 
 import android.app.ProgressDialog;
+import android.bluetooth.BluetoothAdapter;
 import android.content.Context;
+import android.content.Intent;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Bundle;
@@ -10,6 +12,7 @@ import android.support.annotation.NonNull;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
 import android.support.v7.app.ActionBarActivity;
+import android.util.Log;
 import android.view.MenuItem;
 import android.widget.Toast;
 
@@ -19,6 +22,7 @@ import org.fdroid.fdroid.Preferences;
 import org.fdroid.fdroid.R;
 import org.fdroid.fdroid.Utils;
 import org.fdroid.fdroid.localrepo.LocalRepoManager;
+import org.fdroid.fdroid.net.bluetooth.BluetoothServer;
 
 import java.util.Set;
 import java.util.Timer;
@@ -31,6 +35,11 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
     private static final String STATE_JOIN_WIFI = "joinWifi";
     private static final String STATE_NFC = "nfc";
     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 String TAG = "org.fdroid.fdroid.views.swap.SwapActivity";
 
     private Timer shutdownLocalRepoTimer;
     private UpdateAsyncTask updateSwappableAppsTask = null;
@@ -141,14 +150,14 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
         return false;
     }
 
-    private void showBluetooth() {
-
-    }
-
     private void showWifiQr() {
         showFragment(new WifiQrFragment(), STATE_WIFI_QR);
     }
 
+    private void showBluetoothDeviceList() {
+        showFragment(new BluetoothDeviceListFragment(), STATE_BLUETOOTH_DEVICE_LIST);
+    }
+
     private void showFragment(Fragment fragment, String name) {
         getSupportFragmentManager()
                 .beginTransaction()
@@ -215,6 +224,55 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
         finish();
     }
 
+    /**
+     * The process for setting up bluetooth is as follows:
+     *  * Assume we have bluetooth available (otherwise the button which allowed us to start
+     *    the bluetooth process should not have been available). TODO: Remove button if bluetooth unavailable.
+     *  * Ask user to enable (if not enabled yet).
+     *  * Start bluetooth server socket.
+     *  * Enable bluetooth discoverability, so that people can connect to our server socket.
+     *
+     * Note that this is a little different than the usual process for bluetooth _clients_, which
+     * involves pairing and connecting with other devices.
+     */
+    @Override
+    public void connectWithBluetooth() {
+
+        Log.d(TAG, "Initiating Bluetooth swap instead of wifi.");
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        if (adapter.isEnabled()) {
+            Log.d(TAG, "Bluetooth enabled, will pair with device.");
+            startBluetoothServer();
+        } else {
+            Log.d(TAG, "Bluetooth disabled, asking user to enable it.");
+            Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
+            startActivityForResult(enableBtIntent, REQUEST_ENABLE_BLUETOOTH);
+        }
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+
+        if (requestCode == REQUEST_ENABLE_BLUETOOTH) {
+
+            if (resultCode == RESULT_OK) {
+                Log.d(TAG, "User enabled Bluetooth, will pair with device.");
+                startBluetoothServer();
+            } else {
+                // Didn't enable bluetooth
+                Log.d(TAG, "User chose not to enable Bluetooth, so doing nothing (i.e. sticking with wifi).");
+            }
+
+        }
+    }
+
+    private void startBluetoothServer() {
+        Log.d(TAG, "Starting bluetooth server.");
+        new BluetoothServer().start();
+        showBluetoothDeviceList();
+    }
+
     class UpdateAsyncTask extends AsyncTask<Void, String, Void> {
 
         @SuppressWarnings("UnusedDeclaration")
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapProcessManager.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapProcessManager.java
index cb9567bf9..9d2fbc9b4 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapProcessManager.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapProcessManager.java
@@ -9,4 +9,5 @@ package org.fdroid.fdroid.views.swap;
 public interface SwapProcessManager {
     void nextStep();
     void stopSwapping();
+    void connectWithBluetooth();
 }
diff --git a/bluetooth-notes.txt b/bluetooth-notes.txt
new file mode 100644
index 000000000..918d0ca13
--- /dev/null
+++ b/bluetooth-notes.txt
@@ -0,0 +1,21 @@
+One is server, the other is the client (always the case with Bluetooth).
+
+When does the pairing happen? I can think of a few times:
+
+Use case 1 - 
+ * Swapper decides to use bluetooth to send apps to others.
+ * Selects "Use bluetooth instead" on the "join wifi" screen.
+ * Starts a bluetooth server 
+   + Make itself discoverable
+   + Opens a bluetooth server socket
+   + Waits for incoming client connections.
+
+ * Swapee opens swap workflow
+ * Selects the bluetooth option
+ * Is asked to pair with nearby bluetooth devices, using the F-Droid UUID to make sure it doesn't connect to, e.g. bluetooth headphones.
+ * Stays connected in the background
+ * Adds the repo as per usual (with a url such as bluetooth://device-mac-address)
+ * When repo updates, it uses the open connection to get data
+ * If the connection has closed, attempts to reconnect
+ * Same when downloading files
+
diff --git a/res/layout/swap_bluetooth_header.xml b/res/layout/swap_bluetooth_header.xml
new file mode 100644
index 000000000..ae9788b29
--- /dev/null
+++ b/res/layout/swap_bluetooth_header.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout
+    xmlns:tools="http://schemas.android.com/tools"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:gravity="center"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content">
+
+    <TextView
+            android:layout_width="394dp"
+            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"/>
+
+    <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            tools:text="Select from devices below"
+            style="@style/SwapTheme.Wizard.Text"/>
+
+    <ContentLoadingProgressBar
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:id="@+id/loading_indicator"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/src/org/fdroid/fdroid/net/bluetooth/BluetoothDownloader.java b/src/org/fdroid/fdroid/net/BluetoothDownloader.java
similarity index 61%
rename from src/org/fdroid/fdroid/net/bluetooth/BluetoothDownloader.java
rename to src/org/fdroid/fdroid/net/BluetoothDownloader.java
index da66f67d6..c1c770056 100644
--- a/src/org/fdroid/fdroid/net/bluetooth/BluetoothDownloader.java
+++ b/src/org/fdroid/fdroid/net/BluetoothDownloader.java
@@ -1,49 +1,45 @@
-package org.fdroid.fdroid.net.bluetooth;
+package org.fdroid.fdroid.net;
 
 import android.content.Context;
 import android.util.Log;
-import org.fdroid.fdroid.net.Downloader;
+import org.fdroid.fdroid.net.bluetooth.BluetoothClient;
+import org.fdroid.fdroid.net.bluetooth.FileDetails;
 import org.fdroid.fdroid.net.bluetooth.httpish.Request;
 import org.fdroid.fdroid.net.bluetooth.httpish.Response;
 
-import java.io.*;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.net.MalformedURLException;
 
 public class BluetoothDownloader extends Downloader {
 
-    private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothDownloader";
+    private static final String TAG = "org.fdroid.fdroid.net.BluetoothDownloader";
 
     private BluetoothClient client;
     private FileDetails fileDetails;
 
-    public BluetoothDownloader(BluetoothClient client, String destFile, Context ctx) throws FileNotFoundException, MalformedURLException {
+    BluetoothDownloader(BluetoothClient client, String destFile, Context ctx) throws FileNotFoundException, MalformedURLException {
         super(destFile, ctx);
-        this.client = client;
     }
 
-    public BluetoothDownloader(BluetoothClient client, Context ctx) throws IOException {
+    BluetoothDownloader(BluetoothClient client, Context ctx) throws IOException {
         super(ctx);
-        this.client = client;
     }
 
-    public BluetoothDownloader(BluetoothClient client, File destFile) throws FileNotFoundException, MalformedURLException {
+    BluetoothDownloader(BluetoothClient client, File destFile) throws FileNotFoundException, MalformedURLException {
         super(destFile);
-        this.client = client;
     }
 
-    public BluetoothDownloader(BluetoothClient client, File destFile, Context ctx) throws IOException {
-        super(destFile, ctx);
-        this.client = client;
-    }
-
-    public BluetoothDownloader(BluetoothClient client, OutputStream output) throws MalformedURLException {
+    BluetoothDownloader(BluetoothClient client, OutputStream output) throws MalformedURLException {
         super(output);
-        this.client = client;
     }
 
     @Override
-    public InputStream inputStream() throws IOException {
-        Response response = new Request(Request.Methods.GET, client).send();
+    public InputStream getInputStream() throws IOException {
+        Response response = Request.createGET(sourceUrl.getPath(), client.openConnection()).send();
         fileDetails = response.toFileDetails();
         return response.toContentStream();
     }
@@ -58,7 +54,7 @@ public class BluetoothDownloader extends Downloader {
         if (fileDetails == null) {
             Log.d(TAG, "Going to Bluetooth \"server\" to get file details.");
             try {
-                fileDetails = new Request(Request.Methods.HEAD, client).send().toFileDetails();
+                fileDetails = Request.createHEAD(sourceUrl.getPath(), client.openConnection()).send().toFileDetails();
             } catch (IOException e) {
                 Log.e(TAG, "Error getting file details from Bluetooth \"server\": " + e.getMessage());
             }
diff --git a/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java b/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
index 68ddf52e6..4f77b80a1 100644
--- a/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
+++ b/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
@@ -2,22 +2,18 @@ package org.fdroid.fdroid.net.bluetooth;
 
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothSocket;
-import android.util.Log;
-import org.fdroid.fdroid.Utils;
 
-import java.io.*;
-import java.util.UUID;
+import java.io.IOException;
 
 public class BluetoothClient {
 
     private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothClient";
 
-    private BluetoothAdapter adapter;
+    private final BluetoothAdapter adapter;
     private BluetoothDevice device;
 
-    public BluetoothClient(BluetoothAdapter adapter) {
-        this.adapter = adapter;
+    public BluetoothClient() {
+        this.adapter = BluetoothAdapter.getDefaultAdapter();
     }
 
     public void pairWithDevice() throws IOException {
@@ -27,55 +23,15 @@ public class BluetoothClient {
         }
         
         // TODO: Don't just take a random bluetooth device :)
+
         device = adapter.getBondedDevices().iterator().next();
+        device.createRfcommSocketToServiceRecord(BluetoothConstants.fdroidUuid());
 
     }
 
-    public Connection openConnection() throws IOException {
-        return new Connection();
+    public BluetoothConnection openConnection() throws IOException {
+        return null;
+        // return new BluetoothConnection();
     }
 
-    public class Connection {
-
-        private InputStream input = null;
-        private OutputStream output = null;
-
-        private BluetoothSocket socket;
-
-        private Connection() throws IOException {
-            Log.d(TAG, "Attempting to create connection to Bluetooth device '" + device.getName() + "'...");
-            socket = device.createRfcommSocketToServiceRecord(UUID.fromString(BluetoothConstants.fdroidUuid()));
-        }
-
-        public InputStream getInputStream() {
-            return input;
-        }
-
-        public OutputStream getOutputStream() {
-            return output;
-        }
-
-        public void open() throws IOException {
-            socket.connect();
-            input  = socket.getInputStream();
-            output = socket.getOutputStream();
-            Log.d(TAG, "Opened connection to Bluetooth device '" + device.getName() + "'");
-        }
-
-        public void closeQuietly() {
-            Utils.closeQuietly(input);
-            Utils.closeQuietly(output);
-            Utils.closeQuietly(socket);
-        }
-
-        public void close() throws IOException {
-            if (input == null || output == null) {
-                throw new RuntimeException("Cannot close() a BluetoothConnection before calling open()" );
-            }
-
-            input.close();
-            output.close();
-            socket.close();
-        }
-    }
 }
diff --git a/src/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java b/src/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java
new file mode 100644
index 000000000..43ba63d8d
--- /dev/null
+++ b/src/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java
@@ -0,0 +1,61 @@
+package org.fdroid.fdroid.net.bluetooth;
+
+import android.annotation.TargetApi;
+import android.bluetooth.BluetoothSocket;
+import android.os.Build;
+import android.util.Log;
+import org.fdroid.fdroid.Utils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class BluetoothConnection {
+
+    private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothConnection";
+
+    private InputStream input = null;
+    private OutputStream output = null;
+    protected final BluetoothSocket socket;
+
+    public BluetoothConnection(BluetoothSocket socket) throws IOException {
+        this.socket = socket;
+    }
+
+    public InputStream getInputStream() {
+        return input;
+    }
+
+    public OutputStream getOutputStream() {
+        return output;
+    }
+
+    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+    public void open() throws IOException {
+        if (!socket.isConnected()) {
+            // Server sockets will already be connected when they are passed to us,
+            // client sockets require us to call connect().
+            socket.connect();
+        }
+
+        input  = socket.getInputStream();
+        output = socket.getOutputStream();
+        Log.d(TAG, "Opened connection to Bluetooth device");
+    }
+
+    public void closeQuietly() {
+        Utils.closeQuietly(input);
+        Utils.closeQuietly(output);
+        Utils.closeQuietly(socket);
+    }
+
+    public void close() throws IOException {
+        if (input == null || output == null) {
+            throw new RuntimeException("Cannot close() a BluetoothConnection before calling open()" );
+        }
+
+        input.close();
+        output.close();
+        socket.close();
+    }
+}
\ No newline at end of file
diff --git a/src/org/fdroid/fdroid/net/bluetooth/BluetoothConstants.java b/src/org/fdroid/fdroid/net/bluetooth/BluetoothConstants.java
index 7a44a480c..e0876462d 100644
--- a/src/org/fdroid/fdroid/net/bluetooth/BluetoothConstants.java
+++ b/src/org/fdroid/fdroid/net/bluetooth/BluetoothConstants.java
@@ -1,14 +1,16 @@
 package org.fdroid.fdroid.net.bluetooth;
 
+import java.util.UUID;
+
 /**
  * We need some shared information between the client and the server app.
  */
 public class BluetoothConstants {
 
-    public static String fdroidUuid() {
+    public static UUID fdroidUuid() {
         // TODO: Generate a UUID deterministically from, e.g. "org.fdroid.fdroid.net.Bluetooth";
         // This UUID is just from the first example at http://www.ietf.org/rfc/rfc4122.txt
-        return "f81d4fae-7dec-11d0-a765-00a0c91e6bf6";
+        return UUID.fromString("f81d4fae-7dec-11d0-a765-00a0c91e6bf6");
     }
 
 }
diff --git a/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java b/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
new file mode 100644
index 000000000..570f5eba0
--- /dev/null
+++ b/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
@@ -0,0 +1,112 @@
+package org.fdroid.fdroid.net.bluetooth;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothServerSocket;
+import android.bluetooth.BluetoothSocket;
+import android.util.Log;
+import org.fdroid.fdroid.Utils;
+import org.fdroid.fdroid.net.bluetooth.httpish.Request;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Act as a layer on top of LocalHTTPD server, by forwarding requests served
+ * over bluetooth to that server.
+ */
+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>();
+
+    public void close() {
+
+        for (Connection connection : clients) {
+            connection.interrupt();
+        }
+
+        if (serverSocket != null) {
+            Utils.closeQuietly(serverSocket);
+        }
+
+    }
+
+    @Override
+    public void run() {
+
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        try {
+            serverSocket = adapter.listenUsingRfcommWithServiceRecord("FDroid App Swap", BluetoothConstants.fdroidUuid());
+        } catch (IOException e) {
+            Log.e(TAG, "Error starting Bluetooth server socket, will stop the server now - " + e.getMessage());
+            return;
+        }
+
+        while (true) {
+            try {
+                BluetoothSocket clientSocket = serverSocket.accept();
+                if (clientSocket != null && !isInterrupted()) {
+                    Connection client = new Connection(clientSocket);
+                    client.start();
+                    clients.add(client);
+                } else {
+                    break;
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "Error receiving client connection over Bluetooth server socket, will continue listening for other clients - " + e.getMessage());
+            }
+        }
+
+    }
+
+    private static class Connection extends Thread
+    {
+
+        private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothServer.Connection";
+        private BluetoothSocket socket;
+
+        public Connection(BluetoothSocket socket) {
+            this.socket = socket;
+        }
+
+        @Override
+        public void run() {
+
+            Log.d(TAG, "Listening for incoming Bluetooth requests from client");
+
+            BluetoothConnection connection;
+            try {
+                connection = new BluetoothConnection(socket);
+            } catch (IOException e) {
+                Log.e(TAG, "Error listening for incoming connections over bluetooth - " + e.getMessage());
+                return;
+            }
+
+            while (true) {
+
+                try {
+                    Log.d(TAG, "Listening for new Bluetooth request from client.");
+                    Request incomingRequest = Request.listenForRequest(connection);
+                    handleRequest(incomingRequest);
+                } catch (IOException e) {
+                    Log.e(TAG, "Error receiving incoming connection over bluetooth - " + e.getMessage());
+                }
+
+                if (isInterrupted())
+                    break;
+
+            }
+
+        }
+
+        private void handleRequest(Request request) {
+
+            Log.d(TAG, "Received Bluetooth request from client, will process it now.");
+
+        }
+    }
+}
diff --git a/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java b/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java
index 32ae07eef..3066c7142 100644
--- a/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java
+++ b/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java
@@ -1,37 +1,53 @@
 package org.fdroid.fdroid.net.bluetooth.httpish;
 
-import org.fdroid.fdroid.net.bluetooth.BluetoothClient;
+import org.fdroid.fdroid.net.bluetooth.BluetoothConnection;
 
 import java.io.*;
 import java.util.HashMap;
+import java.util.Locale;
 import java.util.Map;
 
 public class Request {
 
+
     public static interface Methods {
         public static final String HEAD = "HEAD";
         public static final String GET  = "GET";
     }
 
-    private final BluetoothClient client;
-    private final String method;
+    private String method;
+    private String path;
+    private Map<String, String> headers;
 
-    private BluetoothClient.Connection connection;
+    private BluetoothConnection connection;
     private BufferedWriter output;
     private BufferedReader input;
 
-    public Request(String method, BluetoothClient client) {
+    private Request(String method, String path, BluetoothConnection connection) {
         this.method = method;
-        this.client = client;
+        this.path = path;
+        this.connection = connection;
+    }
+
+    public static Request createHEAD(String path, BluetoothConnection connection)
+    {
+        return new Request(Methods.HEAD, path, connection);
+    }
+
+    public static Request createGET(String path, BluetoothConnection connection) {
+        return new Request(Methods.GET, path, connection);
     }
 
     public Response send() throws IOException {
 
-        connection = client.openConnection();
         output = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream()));
         input  = new BufferedReader(new InputStreamReader(connection.getInputStream()));
 
         output.write(method);
+        output.write(' ');
+        output.write(path);
+
+        output.write("\n\n");
 
         int responseCode = readResponseCode();
         Map<String, String> headers = readHeaders();
@@ -44,6 +60,40 @@ public class Request {
 
     }
 
+    /**
+     * Helper function used by listenForRequest().
+     * The reason it is here is because the listenForRequest() is a static function, which would
+     * need to instantiate it's own InputReaders from the bluetooth connection. However, we already
+     * have that happening in a Request, so it is in some ways simpler to delegate to a member
+     * method like this.
+     */
+    private boolean listen() throws IOException {
+
+        String requestLine = input.readLine();
+
+        if (requestLine == null || requestLine.trim().length() == 0)
+            return false;
+
+        String[] parts = requestLine.split("\\s+");
+
+        // First part is the method (GET/HEAD), second is the path (/fdroid/repo/index.jar)
+        if (parts.length < 2)
+            return false;
+
+        method  = parts[0].toUpperCase(Locale.ENGLISH);
+        path    = parts[1];
+        headers = readHeaders();
+        return true;
+    }
+
+    /**
+     * This is a blocking method, which will wait until a full Request is received.
+     */
+    public static Request listenForRequest(BluetoothConnection connection) throws IOException {
+        Request request = new Request("", "", connection);
+        return request.listen() ? request : null;
+    }
+
     /**
      * First line of a HTTP response is the status line:
      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1
diff --git a/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java b/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java
new file mode 100644
index 000000000..f9e68699b
--- /dev/null
+++ b/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java
@@ -0,0 +1,111 @@
+package org.fdroid.fdroid.views.swap;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.view.ContextThemeWrapper;
+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.views.fragments.ThemeableListFragment;
+
+import java.util.List;
+
+public class BluetoothDeviceListFragment extends ThemeableListFragment {
+
+    private Adapter adapter = null;
+
+    private class Adapter extends ArrayAdapter<BluetoothDevice> {
+
+        public Adapter(Context context, int resource) {
+            super(context, resource);
+        }
+
+        public Adapter(Context context, int resource, int textViewResourceId) {
+            super(context, resource, textViewResourceId);
+        }
+
+        public Adapter(Context context, int resource, BluetoothDevice[] objects) {
+            super(context, resource, objects);
+        }
+
+        public Adapter(Context context, int resource, int textViewResourceId, BluetoothDevice[] objects) {
+            super(context, resource, textViewResourceId, objects);
+        }
+
+        public Adapter(Context context, int resource, List<BluetoothDevice> objects) {
+            super(context, resource, objects);
+        }
+
+        public Adapter(Context context, int resource, int textViewResourceId, List<BluetoothDevice> objects) {
+            super(context, resource, textViewResourceId, objects);
+        }
+
+        @Override
+        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);
+            } 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);
+
+            nameView.setText(device.getName());
+            descriptionView.setText(device.getAddress());
+
+            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);
+        }
+    }
+
+    @Override
+    protected int getThemeStyle() {
+        return R.style.SwapTheme_StartSwap;
+    }
+
+    @Override
+    protected int getHeaderLayout() {
+        return R.layout.swap_bluetooth_header;
+    }
+}

From 7dff9a9499253f3a161a4fe918c8931ee64c1769 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@ivt.com.au>
Date: Tue, 14 Oct 2014 07:02:19 +1030
Subject: [PATCH 03/16] WIP: Bluetooth communication between devices is up and
 running (not finished).

Devices now make themselves discoverable, and the client sends a test ping.
They UI is not styles properly though, and it doesn't handle the case where
somebody chooses to make their device not-discoverable (because the desired
peer is already paired and it is unneccesary). It also doesn't handle failure
anywhere.
---
 F-Droid/AndroidManifest.xml                   |   3 +-
 F-Droid/res/values/styles.xml                 |  11 +-
 F-Droid/src/org/fdroid/fdroid/FDroidApp.java  | 111 ++++++----
 .../fdroid/localrepo/LocalRepoService.java    | 155 +++----------
 .../org/fdroid/fdroid/net/HttpDownloader.java |  18 +-
 .../src/org/fdroid/fdroid/net/LocalHTTPD.java |   4 +-
 .../fdroid/net/WifiStateChangeService.java    |   2 +-
 .../fdroid/views/LocalRepoActivity.java       |  10 +-
 .../views/QrWizardWifiNetworkActivity.java    |   2 +-
 .../fragments/ThemeableListFragment.java      |  13 +-
 .../fdroid/views/swap/SwapActivity.java       |  56 +++--
 res/layout-v14/simple_list_item_3.xml         |  52 +++++
 res/layout/simple_list_item_3.xml             |  52 +++++
 res/layout/swap_bluetooth_header.xml          |  38 +++-
 .../localrepo/LocalRepoProxyService.java      |  29 +++
 .../localrepo/LocalRepoWifiService.java       | 160 ++++++++++++++
 .../fdroid/net/bluetooth/BluetoothClient.java |  26 +--
 .../fdroid/net/bluetooth/BluetoothServer.java |  45 +++-
 .../fdroid/net/bluetooth/httpish/Request.java |  36 ++-
 .../net/bluetooth/httpish/Response.java       |  82 ++++++-
 .../swap/BluetoothDeviceListFragment.java     | 208 ++++++++++++++----
 21 files changed, 832 insertions(+), 281 deletions(-)
 create mode 100644 res/layout-v14/simple_list_item_3.xml
 create mode 100644 res/layout/simple_list_item_3.xml
 create mode 100644 src/org/fdroid/fdroid/localrepo/LocalRepoProxyService.java
 create mode 100644 src/org/fdroid/fdroid/localrepo/LocalRepoWifiService.java

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;
-    }
 }

From 41b3eab1fdedba72561a1fca074029210769b96f Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@ivt.com.au>
Date: Mon, 20 Oct 2014 23:50:48 +1030
Subject: [PATCH 04/16] WIP: Bluetooth successfully sending entire HTTP
 responses to GET requests.

Now to move the code into a BluetoothDownloader and decide how best
to handle connections, error handling, multiple downloads over one
socket, etc.
---
 .../fdroid/localrepo/LocalRepoService.java    |  3 +-
 .../org/fdroid/fdroid/net/HttpDownloader.java |  4 ++
 .../fdroid/net/WifiStateChangeService.java    |  1 -
 .../localrepo/LocalRepoProxyService.java      |  7 ++++
 .../localrepo/LocalRepoWifiService.java       |  5 +++
 .../fdroid/net/bluetooth/BluetoothServer.java | 12 +++---
 .../net/bluetooth/httpish/Response.java       | 41 ++++++++++++++++---
 .../swap/BluetoothDeviceListFragment.java     |  6 ++-
 8 files changed, 64 insertions(+), 15 deletions(-)

diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoService.java b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoService.java
index 7db6cca4c..2ff16ca5e 100644
--- a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoService.java
+++ b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoService.java
@@ -168,6 +168,7 @@ public abstract class LocalRepoService extends Service {
     }
 
     protected abstract String getIpAddressToBindTo();
+    protected abstract int getPortToBindTo();
 
     protected void startWebServer() {
         Runnable webServer = new Runnable() {
@@ -178,7 +179,7 @@ public abstract class LocalRepoService extends Service {
                 localHttpd = new LocalHTTPD(
                         LocalRepoService.this,
                         getIpAddressToBindTo(),
-                        FDroidApp.port,
+                        getPortToBindTo(),
                         getFilesDir(),
                         useHttps());
 
diff --git a/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java
index af5e5bef5..0387da110 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java
@@ -144,4 +144,8 @@ public class HttpDownloader extends Downloader {
         return this.statusCode != 304;
     }
 
+    public int getStatusCode() {
+        return statusCode;
+    }
+
 }
diff --git a/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java b/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java
index 8ec1527af..8277daa3a 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java
@@ -151,7 +151,6 @@ public class WifiStateChangeService extends Service {
             Intent intent = new Intent(BROADCAST);
             LocalBroadcastManager.getInstance(WifiStateChangeService.this).sendBroadcast(intent);
             WifiStateChangeService.this.stopSelf();
-            FDroidApp.localRepoWifi.restartIfRunning();
         }
     }
 
diff --git a/src/org/fdroid/fdroid/localrepo/LocalRepoProxyService.java b/src/org/fdroid/fdroid/localrepo/LocalRepoProxyService.java
index 7245f9180..af208c0aa 100644
--- a/src/org/fdroid/fdroid/localrepo/LocalRepoProxyService.java
+++ b/src/org/fdroid/fdroid/localrepo/LocalRepoProxyService.java
@@ -1,5 +1,7 @@
 package org.fdroid.fdroid.localrepo;
 
+import org.fdroid.fdroid.FDroidApp;
+
 /**
  * Starts the local repo service but bound to 127.0.0.1.
  * Also, it does not care about whether wifi is connected or not,
@@ -26,4 +28,9 @@ public class LocalRepoProxyService extends LocalRepoService {
     protected String getIpAddressToBindTo() {
         return "127.0.0.1";
     }
+
+    @Override
+    protected int getPortToBindTo() {
+        return FDroidApp.port + 1;
+    }
 }
diff --git a/src/org/fdroid/fdroid/localrepo/LocalRepoWifiService.java b/src/org/fdroid/fdroid/localrepo/LocalRepoWifiService.java
index cc9cafd2a..40f4dd785 100644
--- a/src/org/fdroid/fdroid/localrepo/LocalRepoWifiService.java
+++ b/src/org/fdroid/fdroid/localrepo/LocalRepoWifiService.java
@@ -71,6 +71,11 @@ public class LocalRepoWifiService extends LocalRepoService {
         return FDroidApp.ipAddressString;
     }
 
+    @Override
+    protected int getPortToBindTo() {
+        return FDroidApp.port;
+    }
+
     private Preferences.ChangeListener localRepoBonjourChangeListener = new Preferences.ChangeListener() {
         @Override
         public void onPreferenceChange() {
diff --git a/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java b/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
index 76fb1c57d..111ec3f78 100644
--- a/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
+++ b/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
@@ -5,6 +5,7 @@ import android.bluetooth.BluetoothServerSocket;
 import android.bluetooth.BluetoothSocket;
 import android.content.Context;
 import android.util.Log;
+import org.fdroid.fdroid.FDroidApp;
 import org.fdroid.fdroid.Utils;
 import org.fdroid.fdroid.net.HttpDownloader;
 import org.fdroid.fdroid.net.bluetooth.httpish.Request;
@@ -120,20 +121,21 @@ public class BluetoothServer extends Thread {
             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);
+                HttpDownloader downloader = new HttpDownloader("http://127.0.0.1:" + ( FDroidApp.port + 1 ) + "/" + request.getPath(), context);
 
-                Response.ResponseBuilder builder;
+                Response.Builder builder;
 
                 if (request.getMethod().equals(Request.Methods.HEAD)) {
-                    builder = new Response.ResponseBuilder();
+                    builder = new Response.Builder();
                 } else {
-                    builder = new Response.ResponseBuilder(downloader.getInputStream());
+                    builder = new Response.Builder(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)
+                        .setStatusCode(downloader.getStatusCode())
+                        .setFileSize(downloader.totalDownloadSize())
                         .build();
 
             } catch (IOException e) {
diff --git a/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java b/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java
index 44c8c900e..e98e81a94 100644
--- a/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java
+++ b/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java
@@ -6,6 +6,7 @@ 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.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStreamWriter;
@@ -41,6 +42,17 @@ public class Response {
         return statusCode;
     }
 
+    public int getFileSize() {
+        if (headers != null) {
+            for (Map.Entry<String, String> entry : headers.entrySet()) {
+                if (entry.getKey().toLowerCase().equals("content-length")) {
+                    return Integer.parseInt( entry.getValue()); // TODO: error handling.
+                }
+            }
+        }
+        return -1;
+    }
+
     /**
      * Extracts meaningful headers from the response into a more useful and safe
      * {@link org.fdroid.fdroid.net.bluetooth.FileDetails} object.
@@ -85,30 +97,47 @@ public class Response {
 
     }
 
-    public static class ResponseBuilder {
+    public String readContents() throws IOException {
+        int size = getFileSize();
+        if (contentStream == null || getFileSize() <= 0) {
+            return null;
+        }
+
+        int pos = 0;
+        byte[] buffer = new byte[4096];
+        ByteArrayOutputStream contents = new ByteArrayOutputStream(size);
+        while (pos < size) {
+            int read = contentStream.read(buffer);
+            pos += read;
+            contents.write(buffer, 0, read);
+        }
+        return contents.toString();
+    }
+
+    public static class Builder {
 
         private InputStream contentStream;
         private int statusCode = 200;
         private int fileSize = -1;
         private String etag = null;
 
-        public ResponseBuilder() {}
+        public Builder() {}
 
-        public ResponseBuilder(InputStream contentStream) {
+        public Builder(InputStream contentStream) {
             this.contentStream = contentStream;
         }
 
-        public ResponseBuilder setStatusCode(int statusCode) {
+        public Builder setStatusCode(int statusCode) {
             this.statusCode = statusCode;
             return this;
         }
 
-        public ResponseBuilder setFileSize(int fileSize) {
+        public Builder setFileSize(int fileSize) {
             this.fileSize = fileSize;
             return this;
         }
 
-        public ResponseBuilder setETag(String etag) {
+        public Builder setETag(String etag) {
             this.etag = etag;
             return this;
         }
diff --git a/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java b/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java
index 2467aea31..5affbf4e6 100644
--- a/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java
+++ b/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java
@@ -133,12 +133,14 @@ public class BluetoothDeviceListFragment extends ThemeableListFragment {
             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);
+            Request head = Request.createGET("/", connection);
             Log.d(TAG, "Sending request...");
             Response response = head.send();
             Log.d(TAG, "Response from bluetooth: " + response.getStatusCode());
+            String contents = response.readContents();
+            Log.d(TAG, contents);
         } catch (IOException e) {
-
+            Log.e(TAG, "Error: " + e.getMessage());
         }
 
         /*if (device.getBondState() == BluetoothDevice.BOND_NONE) {

From 5e80b04266648c775f824b7c10c8560290f32f17 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@ivt.com.au>
Date: Wed, 22 Oct 2014 17:49:07 +1030
Subject: [PATCH 05/16] WIP: Improving streme handling for bluetooth swap
 connections.

Brought in an Apache 2.0 licensed file from apache commons to help
with this, which is one of many similar classes online that do the
same thing. At this stage, didn't feel like pulling in all of apache
commons as a dependency.
---
 .../src/org/fdroid/fdroid/net/Downloader.java |   8 +-
 .../commons/io/input/BoundedInputStream.java  | 230 ++++++++++++++++++
 .../fdroid/net/BluetoothDownloader.java       |  41 +++-
 .../swap/BluetoothDeviceListFragment.java     |  23 +-
 4 files changed, 285 insertions(+), 17 deletions(-)
 create mode 100644 src/org/apache/commons/io/input/BoundedInputStream.java

diff --git a/F-Droid/src/org/fdroid/fdroid/net/Downloader.java b/F-Droid/src/org/fdroid/fdroid/net/Downloader.java
index 746fcc1c7..af1c6325c 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/Downloader.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/Downloader.java
@@ -127,7 +127,8 @@ public abstract class Downloader {
             // we were interrupted before proceeding to the download.
             throwExceptionIfInterrupted();
 
-            copyInputToOutputStream(getInputStream());
+            // TODO: Check side effects of changing this second getInputStream() to input.
+            copyInputToOutputStream(input);
         } finally {
             Utils.closeQuietly(outputStream);
             Utils.closeQuietly(input);
@@ -173,12 +174,13 @@ public abstract class Downloader {
             int count = input.read(buffer);
             throwExceptionIfInterrupted();
 
-            bytesRead += count;
-            sendProgress(bytesRead, totalBytes);
             if (count == -1) {
                 Log.d(TAG, "Finished downloading from stream");
                 break;
             }
+
+            bytesRead += count;
+            sendProgress(bytesRead, totalBytes);
             outputStream.write(buffer, 0, count);
         }
         outputStream.flush();
diff --git a/src/org/apache/commons/io/input/BoundedInputStream.java b/src/org/apache/commons/io/input/BoundedInputStream.java
new file mode 100644
index 000000000..f80c1730f
--- /dev/null
+++ b/src/org/apache/commons/io/input/BoundedInputStream.java
@@ -0,0 +1,230 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.commons.io.input;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * This is a stream that will only supply bytes up to a certain length - if its
+ * position goes above that, it will stop.
+ * <p>
+ * This is useful to wrap ServletInputStreams. The ServletInputStream will block
+ * if you try to read content from it that isn't there, because it doesn't know
+ * whether the content hasn't arrived yet or whether the content has finished.
+ * So, one of these, initialized with the Content-length sent in the
+ * ServletInputStream's header, will stop it blocking, providing it's been sent
+ * with a correct content length.
+ *
+ * @version $Id: BoundedInputStream.java 1307462 2012-03-30 15:13:11Z ggregory $
+ * @since 2.0
+ */
+public class BoundedInputStream extends InputStream {
+
+    /** the wrapped input stream */
+    private final InputStream in;
+
+    /** the max length to provide */
+    private final long max;
+
+    /** the number of bytes already returned */
+    private long pos = 0;
+
+    /** the marked position */
+    private long mark = -1;
+
+    /** flag if close shoud be propagated */
+    private boolean propagateClose = true;
+
+    /**
+     * Creates a new <code>BoundedInputStream</code> that wraps the given input
+     * stream and limits it to a certain size.
+     *
+     * @param in The wrapped input stream
+     * @param size The maximum number of bytes to return
+     */
+    public BoundedInputStream(InputStream in, long size) {
+        // Some badly designed methods - eg the servlet API - overload length
+        // such that "-1" means stream finished
+        this.max = size;
+        this.in = in;
+    }
+
+    /**
+     * Creates a new <code>BoundedInputStream</code> that wraps the given input
+     * stream and is unlimited.
+     *
+     * @param in The wrapped input stream
+     */
+    public BoundedInputStream(InputStream in) {
+        this(in, -1);
+    }
+
+    /**
+     * Invokes the delegate's <code>read()</code> method if
+     * the current position is less than the limit.
+     * @return the byte read or -1 if the end of stream or
+     * the limit has been reached.
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public int read() throws IOException {
+        if (max >= 0 && pos >= max) {
+            return -1;
+        }
+        int result = in.read();
+        pos++;
+        return result;
+    }
+
+    /**
+     * Invokes the delegate's <code>read(byte[])</code> method.
+     * @param b the buffer to read the bytes into
+     * @return the number of bytes read or -1 if the end of stream or
+     * the limit has been reached.
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public int read(byte[] b) throws IOException {
+        return this.read(b, 0, b.length);
+    }
+
+    /**
+     * Invokes the delegate's <code>read(byte[], int, int)</code> method.
+     * @param b the buffer to read the bytes into
+     * @param off The start offset
+     * @param len The number of bytes to read
+     * @return the number of bytes read or -1 if the end of stream or
+     * the limit has been reached.
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        if (max>=0 && pos>=max) {
+            return -1;
+        }
+        long maxRead = max>=0 ? Math.min(len, max-pos) : len;
+        int bytesRead = in.read(b, off, (int)maxRead);
+
+        if (bytesRead==-1) {
+            return -1;
+        }
+
+        pos+=bytesRead;
+        return bytesRead;
+    }
+
+    /**
+     * Invokes the delegate's <code>skip(long)</code> method.
+     * @param n the number of bytes to skip
+     * @return the actual number of bytes skipped
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public long skip(long n) throws IOException {
+        long toSkip = max>=0 ? Math.min(n, max-pos) : n;
+        long skippedBytes = in.skip(toSkip);
+        pos+=skippedBytes;
+        return skippedBytes;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int available() throws IOException {
+        if (max>=0 && pos>=max) {
+            return 0;
+        }
+        return in.available();
+    }
+
+    /**
+     * Invokes the delegate's <code>toString()</code> method.
+     * @return the delegate's <code>toString()</code>
+     */
+    @Override
+    public String toString() {
+        return in.toString();
+    }
+
+    /**
+     * Invokes the delegate's <code>close()</code> method
+     * if {@link #isPropagateClose()} is {@code true}.
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public void close() throws IOException {
+        if (propagateClose) {
+            in.close();
+        }
+    }
+
+    /**
+     * Invokes the delegate's <code>reset()</code> method.
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public synchronized void reset() throws IOException {
+        in.reset();
+        pos = mark;
+    }
+
+    /**
+     * Invokes the delegate's <code>mark(int)</code> method.
+     * @param readlimit read ahead limit
+     */
+    @Override
+    public synchronized void mark(int readlimit) {
+        in.mark(readlimit);
+        mark = pos;
+    }
+
+    /**
+     * Invokes the delegate's <code>markSupported()</code> method.
+     * @return true if mark is supported, otherwise false
+     */
+    @Override
+    public boolean markSupported() {
+        return in.markSupported();
+    }
+
+    /**
+     * Indicates whether the {@link #close()} method
+     * should propagate to the underling {@link InputStream}.
+     *
+     * @return {@code true} if calling {@link #close()}
+     * propagates to the <code>close()</code> method of the
+     * underlying stream or {@code false} if it does not.
+     */
+    public boolean isPropagateClose() {
+        return propagateClose;
+    }
+
+    /**
+     * Set whether the {@link #close()} method
+     * should propagate to the underling {@link InputStream}.
+     *
+     * @param propagateClose {@code true} if calling
+     * {@link #close()} propagates to the <code>close()</code>
+     * method of the underlying stream or
+     * {@code false} if it does not.
+     */
+    public void setPropagateClose(boolean propagateClose) {
+        this.propagateClose = propagateClose;
+    }
+}
diff --git a/src/org/fdroid/fdroid/net/BluetoothDownloader.java b/src/org/fdroid/fdroid/net/BluetoothDownloader.java
index c1c770056..5dcf02758 100644
--- a/src/org/fdroid/fdroid/net/BluetoothDownloader.java
+++ b/src/org/fdroid/fdroid/net/BluetoothDownloader.java
@@ -2,7 +2,8 @@ package org.fdroid.fdroid.net;
 
 import android.content.Context;
 import android.util.Log;
-import org.fdroid.fdroid.net.bluetooth.BluetoothClient;
+import org.apache.commons.io.input.BoundedInputStream;
+import org.fdroid.fdroid.net.bluetooth.BluetoothConnection;
 import org.fdroid.fdroid.net.bluetooth.FileDetails;
 import org.fdroid.fdroid.net.bluetooth.httpish.Request;
 import org.fdroid.fdroid.net.bluetooth.httpish.Response;
@@ -18,30 +19,50 @@ public class BluetoothDownloader extends Downloader {
 
     private static final String TAG = "org.fdroid.fdroid.net.BluetoothDownloader";
 
-    private BluetoothClient client;
+    private final BluetoothConnection connection;
     private FileDetails fileDetails;
+    private final String sourcePath;
 
-    BluetoothDownloader(BluetoothClient client, String destFile, Context ctx) throws FileNotFoundException, MalformedURLException {
+    public BluetoothDownloader(BluetoothConnection connection, String sourcePath, String destFile, Context ctx) throws FileNotFoundException, MalformedURLException {
         super(destFile, ctx);
+        this.connection = connection;
+        this.sourcePath = sourcePath;
     }
 
-    BluetoothDownloader(BluetoothClient client, Context ctx) throws IOException {
+    public BluetoothDownloader(BluetoothConnection connection, String sourcePath, Context ctx) throws IOException {
         super(ctx);
+        this.connection = connection;
+        this.sourcePath = sourcePath;
     }
 
-    BluetoothDownloader(BluetoothClient client, File destFile) throws FileNotFoundException, MalformedURLException {
+    public BluetoothDownloader(BluetoothConnection connection, String sourcePath, File destFile) throws FileNotFoundException, MalformedURLException {
         super(destFile);
+        this.connection = connection;
+        this.sourcePath = sourcePath;
     }
 
-    BluetoothDownloader(BluetoothClient client, OutputStream output) throws MalformedURLException {
+    public BluetoothDownloader(BluetoothConnection connection, String sourcePath, OutputStream output) throws MalformedURLException {
         super(output);
+        this.connection = connection;
+        this.sourcePath = sourcePath;
     }
 
     @Override
     public InputStream getInputStream() throws IOException {
-        Response response = Request.createGET(sourceUrl.getPath(), client.openConnection()).send();
+        Response response = Request.createGET(sourcePath, connection).send();
         fileDetails = response.toFileDetails();
-        return response.toContentStream();
+
+        // TODO: Manage the dependency which includes this class better?
+        // Right now, I only needed the one class from apache commons.
+        // There are countless classes online which provide this functionaligy,
+        // including some which are available from the Android SDK - the only
+        // problem is that they have a funky API which doesn't just wrap a
+        // plain old InputStream (the class is ContentLengthInputStream -
+        // whereas this BoundedInputStream is much more generic and useful
+        // to us).
+        BoundedInputStream stream = new BoundedInputStream(response.toContentStream(), fileDetails.getFileSize());
+        stream.setPropagateClose(false);
+        return stream;
     }
 
     /**
@@ -54,7 +75,7 @@ public class BluetoothDownloader extends Downloader {
         if (fileDetails == null) {
             Log.d(TAG, "Going to Bluetooth \"server\" to get file details.");
             try {
-                fileDetails = Request.createHEAD(sourceUrl.getPath(), client.openConnection()).send().toFileDetails();
+                fileDetails = Request.createHEAD(sourceUrl.getPath(), connection).send().toFileDetails();
             } catch (IOException e) {
                 Log.e(TAG, "Error getting file details from Bluetooth \"server\": " + e.getMessage());
             }
@@ -64,7 +85,7 @@ public class BluetoothDownloader extends Downloader {
 
     @Override
     public boolean hasChanged() {
-        return getFileDetails().getCacheTag().equals(getCacheTag());
+        return getFileDetails().getCacheTag() == null || getFileDetails().getCacheTag().equals(getCacheTag());
     }
 
     @Override
diff --git a/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java b/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java
index 5affbf4e6..dcd0ccdaf 100644
--- a/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java
+++ b/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java
@@ -18,12 +18,12 @@ import android.widget.ArrayAdapter;
 import android.widget.ListView;
 import android.widget.TextView;
 import org.fdroid.fdroid.R;
+import org.fdroid.fdroid.net.BluetoothDownloader;
 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.ByteArrayOutputStream;
 import java.io.IOException;
 import java.util.List;
 
@@ -132,13 +132,28 @@ public class BluetoothDeviceListFragment extends ThemeableListFragment {
         try {
             Log.d(TAG, "Testing bluetooth connection (opening connection first).");
             BluetoothConnection connection = client.openConnection();
-            Log.d(TAG, "Creating HEAD request for resource at \"/\"...");
+
+            ByteArrayOutputStream stream = new ByteArrayOutputStream(4096);
+            BluetoothDownloader downloader = new BluetoothDownloader(connection, "/", stream);
+            downloader.downloadUninterrupted();
+            String result = stream.toString();
+            Log.d(TAG, "Download complete.");
+            Log.d(TAG, result);
+
+            Log.d(TAG, "Downloading again...");
+            downloader = new BluetoothDownloader(connection, "/fdroid/repo/index.xml", stream);
+            downloader.downloadUninterrupted();
+            result = stream.toString();
+            Log.d(TAG, "Download complete.");
+            Log.d(TAG, result);
+
+            /*Log.d(TAG, "Creating HEAD request for resource at \"/\"...");
             Request head = Request.createGET("/", connection);
             Log.d(TAG, "Sending request...");
             Response response = head.send();
             Log.d(TAG, "Response from bluetooth: " + response.getStatusCode());
             String contents = response.readContents();
-            Log.d(TAG, contents);
+            Log.d(TAG, contents);*/
         } catch (IOException e) {
             Log.e(TAG, "Error: " + e.getMessage());
         }

From d7b7af76b7c6c8ddfed919c5a056577dc9e66abb Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Sat, 11 Apr 2015 22:00:06 +1000
Subject: [PATCH 06/16] Cleanup after rebase.

---
 {res => F-Droid/res}/layout-v14/simple_list_item_3.xml            | 0
 {res => F-Droid/res}/layout/simple_list_item_3.xml                | 0
 {res => F-Droid/res}/layout/swap_bluetooth_header.xml             | 0
 .../src}/org/apache/commons/io/input/BoundedInputStream.java      | 0
 .../src}/org/fdroid/fdroid/localrepo/LocalRepoProxyService.java   | 0
 .../src}/org/fdroid/fdroid/localrepo/LocalRepoWifiService.java    | 0
 .../src}/org/fdroid/fdroid/net/BluetoothDownloader.java           | 0
 .../src}/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java     | 0
 .../src}/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java | 0
 .../src}/org/fdroid/fdroid/net/bluetooth/BluetoothConstants.java  | 0
 .../src}/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java     | 0
 .../src}/org/fdroid/fdroid/net/bluetooth/FileDetails.java         | 0
 .../fdroid/fdroid/net/bluetooth/UnexpectedResponseException.java  | 0
 .../src}/org/fdroid/fdroid/net/bluetooth/httpish/Request.java     | 0
 .../src}/org/fdroid/fdroid/net/bluetooth/httpish/Response.java    | 0
 .../fdroid/net/bluetooth/httpish/headers/ContentLengthHeader.java | 0
 .../fdroid/fdroid/net/bluetooth/httpish/headers/ETagHeader.java   | 0
 .../org/fdroid/fdroid/net/bluetooth/httpish/headers/Header.java   | 0
 .../org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java | 0
 19 files changed, 0 insertions(+), 0 deletions(-)
 rename {res => F-Droid/res}/layout-v14/simple_list_item_3.xml (100%)
 rename {res => F-Droid/res}/layout/simple_list_item_3.xml (100%)
 rename {res => F-Droid/res}/layout/swap_bluetooth_header.xml (100%)
 rename {src => F-Droid/src}/org/apache/commons/io/input/BoundedInputStream.java (100%)
 rename {src => F-Droid/src}/org/fdroid/fdroid/localrepo/LocalRepoProxyService.java (100%)
 rename {src => F-Droid/src}/org/fdroid/fdroid/localrepo/LocalRepoWifiService.java (100%)
 rename {src => F-Droid/src}/org/fdroid/fdroid/net/BluetoothDownloader.java (100%)
 rename {src => F-Droid/src}/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java (100%)
 rename {src => F-Droid/src}/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java (100%)
 rename {src => F-Droid/src}/org/fdroid/fdroid/net/bluetooth/BluetoothConstants.java (100%)
 rename {src => F-Droid/src}/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java (100%)
 rename {src => F-Droid/src}/org/fdroid/fdroid/net/bluetooth/FileDetails.java (100%)
 rename {src => F-Droid/src}/org/fdroid/fdroid/net/bluetooth/UnexpectedResponseException.java (100%)
 rename {src => F-Droid/src}/org/fdroid/fdroid/net/bluetooth/httpish/Request.java (100%)
 rename {src => F-Droid/src}/org/fdroid/fdroid/net/bluetooth/httpish/Response.java (100%)
 rename {src => F-Droid/src}/org/fdroid/fdroid/net/bluetooth/httpish/headers/ContentLengthHeader.java (100%)
 rename {src => F-Droid/src}/org/fdroid/fdroid/net/bluetooth/httpish/headers/ETagHeader.java (100%)
 rename {src => F-Droid/src}/org/fdroid/fdroid/net/bluetooth/httpish/headers/Header.java (100%)
 rename {src => F-Droid/src}/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java (100%)

diff --git a/res/layout-v14/simple_list_item_3.xml b/F-Droid/res/layout-v14/simple_list_item_3.xml
similarity index 100%
rename from res/layout-v14/simple_list_item_3.xml
rename to F-Droid/res/layout-v14/simple_list_item_3.xml
diff --git a/res/layout/simple_list_item_3.xml b/F-Droid/res/layout/simple_list_item_3.xml
similarity index 100%
rename from res/layout/simple_list_item_3.xml
rename to F-Droid/res/layout/simple_list_item_3.xml
diff --git a/res/layout/swap_bluetooth_header.xml b/F-Droid/res/layout/swap_bluetooth_header.xml
similarity index 100%
rename from res/layout/swap_bluetooth_header.xml
rename to F-Droid/res/layout/swap_bluetooth_header.xml
diff --git a/src/org/apache/commons/io/input/BoundedInputStream.java b/F-Droid/src/org/apache/commons/io/input/BoundedInputStream.java
similarity index 100%
rename from src/org/apache/commons/io/input/BoundedInputStream.java
rename to F-Droid/src/org/apache/commons/io/input/BoundedInputStream.java
diff --git a/src/org/fdroid/fdroid/localrepo/LocalRepoProxyService.java b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoProxyService.java
similarity index 100%
rename from src/org/fdroid/fdroid/localrepo/LocalRepoProxyService.java
rename to F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoProxyService.java
diff --git a/src/org/fdroid/fdroid/localrepo/LocalRepoWifiService.java b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoWifiService.java
similarity index 100%
rename from src/org/fdroid/fdroid/localrepo/LocalRepoWifiService.java
rename to F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoWifiService.java
diff --git a/src/org/fdroid/fdroid/net/BluetoothDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/BluetoothDownloader.java
similarity index 100%
rename from src/org/fdroid/fdroid/net/BluetoothDownloader.java
rename to F-Droid/src/org/fdroid/fdroid/net/BluetoothDownloader.java
diff --git a/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
similarity index 100%
rename from src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
rename to F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
diff --git a/src/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java
similarity index 100%
rename from src/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java
rename to F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java
diff --git a/src/org/fdroid/fdroid/net/bluetooth/BluetoothConstants.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothConstants.java
similarity index 100%
rename from src/org/fdroid/fdroid/net/bluetooth/BluetoothConstants.java
rename to F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothConstants.java
diff --git a/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
similarity index 100%
rename from src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
rename to F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
diff --git a/src/org/fdroid/fdroid/net/bluetooth/FileDetails.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/FileDetails.java
similarity index 100%
rename from src/org/fdroid/fdroid/net/bluetooth/FileDetails.java
rename to F-Droid/src/org/fdroid/fdroid/net/bluetooth/FileDetails.java
diff --git a/src/org/fdroid/fdroid/net/bluetooth/UnexpectedResponseException.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/UnexpectedResponseException.java
similarity index 100%
rename from src/org/fdroid/fdroid/net/bluetooth/UnexpectedResponseException.java
rename to F-Droid/src/org/fdroid/fdroid/net/bluetooth/UnexpectedResponseException.java
diff --git a/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java
similarity index 100%
rename from src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java
rename to F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java
diff --git a/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java
similarity index 100%
rename from src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java
rename to F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java
diff --git a/src/org/fdroid/fdroid/net/bluetooth/httpish/headers/ContentLengthHeader.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/headers/ContentLengthHeader.java
similarity index 100%
rename from src/org/fdroid/fdroid/net/bluetooth/httpish/headers/ContentLengthHeader.java
rename to F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/headers/ContentLengthHeader.java
diff --git a/src/org/fdroid/fdroid/net/bluetooth/httpish/headers/ETagHeader.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/headers/ETagHeader.java
similarity index 100%
rename from src/org/fdroid/fdroid/net/bluetooth/httpish/headers/ETagHeader.java
rename to F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/headers/ETagHeader.java
diff --git a/src/org/fdroid/fdroid/net/bluetooth/httpish/headers/Header.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/headers/Header.java
similarity index 100%
rename from src/org/fdroid/fdroid/net/bluetooth/httpish/headers/Header.java
rename to F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/headers/Header.java
diff --git a/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java b/F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java
similarity index 100%
rename from src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java
rename to F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java

From 6f3ca8b9c4134ed887fd908041ec7b252aeb545c Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Sun, 12 Apr 2015 00:14:28 +1000
Subject: [PATCH 07/16] WIP: Making 'scan' UI more solid

---
 F-Droid/res/layout/swap_bluetooth_header.xml  |  44 ++++----
 F-Droid/res/menu/swap_scan.xml                |  14 +++
 F-Droid/res/values/strings.xml                |   2 +
 F-Droid/res/values/styles.xml                 |   4 +-
 .../swap/BluetoothDeviceListFragment.java     | 101 +++++++++++++++---
 5 files changed, 130 insertions(+), 35 deletions(-)
 create mode 100644 F-Droid/res/menu/swap_scan.xml

diff --git a/F-Droid/res/layout/swap_bluetooth_header.xml b/F-Droid/res/layout/swap_bluetooth_header.xml
index 3977d3c86..89992d1ae 100644
--- a/F-Droid/res/layout/swap_bluetooth_header.xml
+++ b/F-Droid/res/layout/swap_bluetooth_header.xml
@@ -12,42 +12,50 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             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"/>
+            android:text="@string/swap_bluetooth_your_device"
+            android:gravity="center_horizontal"
+            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"
             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"/>
+            tools:text="Phone v2.0"
+            android:gravity="center_horizontal"
+            style="@style/SwapTheme.BluetoothDeviceList.Heading"
+            android:paddingBottom="10dp"/>
 
     <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"/>
+            android:gravity="center_horizontal"
+            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:text="@string/swap_bluetooth_select_or_scan"
+            android:gravity="center_horizontal"
+            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_width="match_parent"
             android:layout_height="wrap_content"
-            android:id="@+id/loading_indicator"/>
+            android:id="@+id/loading_indicator"
+            android:paddingLeft="5dp"
+            android:paddingRight="5dp"
+            android:visibility="gone"
+            android:indeterminate="true"
+            style="?android:attr/android:progressBarStyleHorizontal"/>
 
 </LinearLayout>
\ No newline at end of file
diff --git a/F-Droid/res/menu/swap_scan.xml b/F-Droid/res/menu/swap_scan.xml
new file mode 100644
index 000000000..ae932ccd2
--- /dev/null
+++ b/F-Droid/res/menu/swap_scan.xml
@@ -0,0 +1,14 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+    <item
+        android:id="@+id/action_scan"
+        android:title="Scan for devices"
+        android:titleCondensed="Scan"/>
+
+    <item
+        android:id="@+id/action_cancel"
+        android:title="Cancel scan"
+        android:titleCondensed="@string/cancel"
+        android:visible="false"/>
+
+</menu>
\ No newline at end of file
diff --git a/F-Droid/res/values/strings.xml b/F-Droid/res/values/strings.xml
index 5da5d0d1f..fb94347b9 100644
--- a/F-Droid/res/values/strings.xml
+++ b/F-Droid/res/values/strings.xml
@@ -329,4 +329,6 @@
     <string name="wifi_warning_public">May work</string>
     <string name="wifi_warning_private">Promising</string>
     <string name="wifi_warning_personal">Best bet</string>
+    <string name="swap_bluetooth_your_device">Your device is</string>
+    <string name="swap_bluetooth_select_or_scan">Select from devices below, or press \"Scan\" from the menu to find more devices.</string>
 </resources>
diff --git a/F-Droid/res/values/styles.xml b/F-Droid/res/values/styles.xml
index 0a2dcc8b2..37496f601 100644
--- a/F-Droid/res/values/styles.xml
+++ b/F-Droid/res/values/styles.xml
@@ -46,10 +46,10 @@
     <style name="SwapTheme.StartSwap.Text" parent="@style/SwapTheme.StartSwap">
     </style>
 
-    <style name="SwapTheme.BluetoothDeviceList" parent="@style/SwapTheme.StartSwap">
+    <style name="SwapTheme.BluetoothDeviceList" parent="@style/SwapTheme.Wizard">
     </style>
 
-    <style name="SwapTheme.BluetoothDeviceList.ListItem" parent="AppThemeLightWithDarkActionBar">
+    <style name="SwapTheme.BluetoothDeviceList.ListItem" parent="AppThemeDark">
     </style>
 
     <style name="SwapTheme.BluetoothDeviceList.Text" parent="@style/SwapTheme.BluetoothDeviceList">
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java b/F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java
index dcd0ccdaf..7719767b0 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java
@@ -7,10 +7,14 @@ import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.os.Bundle;
+import android.support.v4.view.MenuItemCompat;
 import android.support.v4.widget.ContentLoadingProgressBar;
 import android.util.Log;
 import android.view.ContextThemeWrapper;
 import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
@@ -29,14 +33,42 @@ import java.util.List;
 
 public class BluetoothDeviceListFragment extends ThemeableListFragment {
 
-    private static final String TAG = "org.fdroid.fdroid.views.swap.BluetoothDeviceListFragment";
+    private static final String TAG = "fdroid.BluetoothList";
 
     private Adapter adapter = null;
 
+    private MenuItem scanMenuItem;
+    private MenuItem cancelMenuItem;
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        setHasOptionsMenu(false);
+        setHasOptionsMenu(true);
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+        menuInflater.inflate(R.menu.swap_scan, menu);
+
+        final int flags = MenuItemCompat.SHOW_AS_ACTION_ALWAYS | MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT;
+
+        scanMenuItem = menu.findItem(R.id.action_scan);
+        scanMenuItem.setVisible(true);
+        MenuItemCompat.setShowAsAction(scanMenuItem, flags);
+
+        cancelMenuItem = menu.findItem(R.id.action_cancel);
+        cancelMenuItem.setVisible(false);
+        MenuItemCompat.setShowAsAction(cancelMenuItem, flags);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == R.id.action_scan) {
+            initiateBluetoothScan();
+        } else if (item.getItemId() == R.id.action_cancel) {
+            cancelBluetoothScan();
+        }
+        return super.onOptionsItemSelected(item);
     }
 
     @Override
@@ -50,7 +82,7 @@ public class BluetoothDeviceListFragment extends ThemeableListFragment {
             R.layout.select_local_apps_list_item
         );
 
-        populateDeviceList();
+        populateBondedDevices();
 
         setListAdapter(adapter);
         setListShown(false); // start out with a progress indicator
@@ -63,25 +95,54 @@ public class BluetoothDeviceListFragment extends ThemeableListFragment {
         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;
-                }
-            });
+            final BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
+
+            final TextView deviceName = (TextView)headerView.findViewById(R.id.device_name);
+            deviceName.setText(bluetooth.getName());
+
+            final TextView address = (TextView)headerView.findViewById(R.id.device_address);
+            address.setText(bluetooth.getAddress());
         }
         return view;
     }
 
+    private void cancelBluetoothScan() {
+
+        Log.d(TAG, "Cancelling bluetooth scan.");
+
+        cancelMenuItem.setVisible(false);
+        scanMenuItem.setVisible(true);
+
+        final BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
+        bluetooth.cancelDiscovery();
+
+        getLoadingIndicator().hide();
+
+    }
+
+    private ContentLoadingProgressBar getLoadingIndicator() {
+        return ((ContentLoadingProgressBar)getView().findViewById(R.id.loading_indicator));
+    }
+
     private void initiateBluetoothScan()
     {
-        BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
-        BroadcastReceiver deviceFoundReceiver = new BroadcastReceiver() {
+        Log.d(TAG, "Starting bluetooth scan...");
+
+        cancelMenuItem.setVisible(true);
+        scanMenuItem.setVisible(false);
+
+        final ContentLoadingProgressBar loadingBar = getLoadingIndicator();
+
+        loadingBar.show();
+
+
+        final BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
+        final 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);
+                    Log.d(TAG, "Found bluetooth device: " + device.toString());
                     boolean exists = false;
                     for (int i = 0; i < adapter.getCount(); i ++) {
                         if (adapter.getItem(i).getAddress().equals(device.getAddress())) {
@@ -97,8 +158,18 @@ public class BluetoothDeviceListFragment extends ThemeableListFragment {
             }
         };
 
-        ((ContentLoadingProgressBar)getView().findViewById(R.id.loading_indicator)).show();
+        final BroadcastReceiver scanCompleteReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                Log.d(TAG, "Scan complete: " + intent.getAction());
+                loadingBar.hide();
+                cancelMenuItem.setVisible(false);
+                scanMenuItem.setVisible(true);
+            }
+        };
+
         getActivity().registerReceiver(deviceFoundReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND));
+        getActivity().registerReceiver(scanCompleteReceiver, new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED));
 
         if (!bluetooth.startDiscovery()) {
             // TODO: Discovery did not start for some reason :(
@@ -106,7 +177,7 @@ public class BluetoothDeviceListFragment extends ThemeableListFragment {
         }
     }
 
-    private void populateDeviceList()
+    private void populateBondedDevices()
     {
         for (BluetoothDevice device : BluetoothAdapter.getDefaultAdapter().getBondedDevices()) {
             adapter.add(device);
@@ -220,7 +291,7 @@ public class BluetoothDeviceListFragment extends ThemeableListFragment {
             TextView addressView = (TextView)view.findViewById(android.R.id.text2);
             TextView descriptionView = (TextView)view.findViewById(R.id.text3);
 
-            nameView.setText(device.getName());
+            nameView.setText(device.getName() == null ? getString(R.string.unknown) : device.getName());
             addressView.setText(device.getAddress());
             descriptionView.setText(bondStateToLabel(device.getBondState()));
 

From cb73e6352c4f92915aa29ccd9134dc222b9833f4 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Thu, 14 May 2015 10:00:11 +1000
Subject: [PATCH 08/16] Cleaned up some lint errors, i18n some strings.

---
 F-Droid/res/values/strings.xml                    |  7 +++++--
 .../fdroid/fdroid/net/BluetoothDownloader.java    | 12 +++---------
 .../fdroid/fdroid/net/WifiStateChangeService.java |  1 +
 .../fdroid/net/bluetooth/BluetoothClient.java     |  3 ++-
 .../fdroid/net/bluetooth/BluetoothConnection.java |  2 +-
 .../fdroid/net/bluetooth/BluetoothServer.java     | 15 ++++++++++-----
 .../fdroid/net/bluetooth/httpish/Request.java     | 14 +++++++++-----
 .../fdroid/net/bluetooth/httpish/Response.java    |  2 +-
 .../views/swap/BluetoothDeviceListFragment.java   |  6 +++---
 .../fdroid/fdroid/views/swap/SwapActivity.java    |  3 ++-
 10 files changed, 37 insertions(+), 28 deletions(-)

diff --git a/F-Droid/res/values/strings.xml b/F-Droid/res/values/strings.xml
index fb94347b9..77aebe2fe 100644
--- a/F-Droid/res/values/strings.xml
+++ b/F-Droid/res/values/strings.xml
@@ -329,6 +329,9 @@
     <string name="wifi_warning_public">May work</string>
     <string name="wifi_warning_private">Promising</string>
     <string name="wifi_warning_personal">Best bet</string>
-    <string name="swap_bluetooth_your_device">Your device is</string>
-    <string name="swap_bluetooth_select_or_scan">Select from devices below, or press \"Scan\" from the menu to find more devices.</string>
+	<string name="swap_bluetooth_your_device">Your device is</string>
+	<string name="swap_bluetooth_select_or_scan">Select from devices below, or press \"Scan\" from the menu to find more devices.</string>
+	<string name="swap_bluetooth_bonded_device">Bonded</string>
+	<string name="swap_bluetooth_bonding_device">Currently bonding...</string>
+	<string name="swap_bluetooth_unknown_device">Unknown device</string>
 </resources>
diff --git a/F-Droid/src/org/fdroid/fdroid/net/BluetoothDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/BluetoothDownloader.java
index 5dcf02758..a860e2aa2 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/BluetoothDownloader.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/BluetoothDownloader.java
@@ -17,18 +17,12 @@ import java.net.MalformedURLException;
 
 public class BluetoothDownloader extends Downloader {
 
-    private static final String TAG = "org.fdroid.fdroid.net.BluetoothDownloader";
+    private static final String TAG = "BluetoothDownloader";
 
     private final BluetoothConnection connection;
     private FileDetails fileDetails;
     private final String sourcePath;
 
-    public BluetoothDownloader(BluetoothConnection connection, String sourcePath, String destFile, Context ctx) throws FileNotFoundException, MalformedURLException {
-        super(destFile, ctx);
-        this.connection = connection;
-        this.sourcePath = sourcePath;
-    }
-
     public BluetoothDownloader(BluetoothConnection connection, String sourcePath, Context ctx) throws IOException {
         super(ctx);
         this.connection = connection;
@@ -54,7 +48,7 @@ public class BluetoothDownloader extends Downloader {
 
         // TODO: Manage the dependency which includes this class better?
         // Right now, I only needed the one class from apache commons.
-        // There are countless classes online which provide this functionaligy,
+        // There are countless classes online which provide this functionality,
         // including some which are available from the Android SDK - the only
         // problem is that they have a funky API which doesn't just wrap a
         // plain old InputStream (the class is ContentLengthInputStream -
@@ -67,7 +61,7 @@ public class BluetoothDownloader extends Downloader {
 
     /**
      * May return null if an error occurred while getting file details.
-     * TODO: Should we throw an exception? Everywhere else in this blue package throws IO exceptions weely neely.
+     * TODO: Should we throw an exception? Everywhere else in this blue package throws IO exceptions weelx`x`xy-neely.
      * Will probably require some thought as to how the API looks, with regards to all of the public methods
      * and their signatures.
      */
diff --git a/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java b/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java
index 8277daa3a..8ec1527af 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java
@@ -151,6 +151,7 @@ public class WifiStateChangeService extends Service {
             Intent intent = new Intent(BROADCAST);
             LocalBroadcastManager.getInstance(WifiStateChangeService.this).sendBroadcast(intent);
             WifiStateChangeService.this.stopSelf();
+            FDroidApp.localRepoWifi.restartIfRunning();
         }
     }
 
diff --git a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
index 886b215f6..fe45b8ee3 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
@@ -7,7 +7,8 @@ import java.io.IOException;
 
 public class BluetoothClient {
 
-    private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothClient";
+    @SuppressWarnings("unused")
+    private static final String TAG = "BluetoothClient";
 
     private BluetoothDevice device;
 
diff --git a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java
index 43ba63d8d..c74f1e3cd 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java
@@ -12,7 +12,7 @@ import java.io.OutputStream;
 
 public class BluetoothConnection {
 
-    private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothConnection";
+    private static final String TAG = "BluetoothConnection";
 
     private InputStream input = null;
     private OutputStream output = null;
diff --git a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
index 111ec3f78..493cc47be 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
@@ -4,6 +4,7 @@ import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothServerSocket;
 import android.bluetooth.BluetoothSocket;
 import android.content.Context;
+import android.os.Build;
 import android.util.Log;
 import org.fdroid.fdroid.FDroidApp;
 import org.fdroid.fdroid.Utils;
@@ -21,10 +22,10 @@ import java.util.List;
  */
 public class BluetoothServer extends Thread {
 
-    private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothServer";
+    private static final String TAG = "BluetoothServer";
 
     private BluetoothServerSocket serverSocket;
-    private List<Connection> clients = new ArrayList<Connection>();
+    private List<Connection> clients = new ArrayList<>();
 
     private final Context context;
 
@@ -75,8 +76,6 @@ public class BluetoothServer extends Thread {
     private static class Connection extends Thread
     {
 
-        private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothServer.Connection";
-
         private final Context context;
         private final BluetoothSocket socket;
 
@@ -139,7 +138,13 @@ public class BluetoothServer extends Thread {
                         .build();
 
             } catch (IOException e) {
-                throw new IOException("Error getting file " + request.getPath() + " from local repo proxy - " + e.getMessage(), e);
+                if (Build.VERSION.SDK_INT <= 9) {
+                    // Would like to use the specific IOException below with a "cause", but it is
+                    // only supported on SDK 9, so I guess this is the next most useful thing.
+                    throw e;
+                } else {
+                    throw new IOException("Error getting file " + request.getPath() + " from local repo proxy - " + e.getMessage(), e);
+                }
             }
 
         }
diff --git a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java
index be10cd4e3..f78c981c4 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java
@@ -15,11 +15,11 @@ import java.util.Map;
 public class Request {
 
 
-    private static final String TAG = "org.fdroid.fdroid.net.bluetooth.httpish.Request";
+    private static final String TAG = "bluetooth.Request";
 
-    public static interface Methods {
-        public static final String HEAD = "HEAD";
-        public static final String GET  = "GET";
+    public interface Methods {
+        String HEAD = "HEAD";
+        String GET  = "GET";
     }
 
     private String method;
@@ -48,6 +48,10 @@ public class Request {
         return new Request(Methods.GET, path, connection);
     }
 
+    public String getHeaderValue(String header) {
+        return headers.containsKey(header) ? headers.get(header) : null;
+    }
+
     public Response send() throws IOException {
 
         Log.d(TAG, "Sending request to server (" + path + ")");
@@ -141,7 +145,7 @@ public class Request {
      * our HTTP-ish implementation.
      */
     private Map<String, String> readHeaders() throws IOException {
-        Map<String, String> headers = new HashMap<String, String>();
+        Map<String, String> headers = new HashMap<>();
         String responseLine = input.readLine();
         while (responseLine != null && responseLine.length() > 0) {
 
diff --git a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java
index e98e81a94..3e5b274b3 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java
@@ -16,7 +16,7 @@ import java.util.Map;
 
 public class Response {
 
-    private static final String TAG = "org.fdroid.fdroid.net.bluetooth.httpish.Response";
+    private static final String TAG = "bluetooth.Response";
 
     private int statusCode;
     private Map<String, String> headers;
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java b/F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java
index 7719767b0..a13d69c08 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java
@@ -304,12 +304,12 @@ public class BluetoothDeviceListFragment extends ThemeableListFragment {
                 // 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";
+                return getString(R.string.swap_bluetooth_bonded_device);
             } else if (deviceBondState == BluetoothDevice.BOND_BONDING) {
-                return "Currently bonding...";
+                return getString(R.string.swap_bluetooth_bonding_device);
             } else {
                 // TODO: Might be a little bit harsh, makes it sound more malicious than it should.
-                return "Unknown device";
+                return getString(R.string.swap_bluetooth_unknown_device);
             }
         }
     }
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 47429edf4..d9499b0fd 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java
@@ -40,7 +40,7 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
     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";
+    private static final String TAG = "SwapActivity";
 
     private Timer shutdownLocalRepoTimer;
     private UpdateAsyncTask updateSwappableAppsTask = null;
@@ -241,6 +241,7 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
 
         Log.d(TAG, "Initiating Bluetooth swap instead of wifi.");
         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        // TODO: May be null (e.g. on an emulator).
         if (adapter.isEnabled()) {
             Log.d(TAG, "Bluetooth enabled, will pair with device.");
             ensureBluetoothDiscoverable();

From 92430e163e1777bbee9447e59fab5ea8dbe0d165 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Thu, 14 May 2015 18:40:25 +1000
Subject: [PATCH 09/16] WIP

---
 .../fdroid/fdroid/net/DownloaderFactory.java  | 27 +++++++++++++------
 bluetooth-notes.txt                           | 21 ---------------
 2 files changed, 19 insertions(+), 29 deletions(-)
 delete mode 100644 bluetooth-notes.txt

diff --git a/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java b/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java
index a6cae1438..59192b932 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java
@@ -1,26 +1,37 @@
 package org.fdroid.fdroid.net;
 
 import android.content.Context;
+import android.net.Uri;
 
 import java.io.File;
 import java.io.IOException;
 
 public class DownloaderFactory {
 
-    public static Downloader create(String url, Context context)
-            throws IOException {
-        if (isOnionAddress(url)) {
+    public static Downloader create(String url, Context context) throws IOException {
+        Uri uri = Uri.parse(url);
+        if (isBluetoothAddress(uri)) {
+            return new BluetoothDownloader(null, uri.getPath(), context);
+        } else if (isOnionAddress(url)) {
             return new TorHttpDownloader(url, context);
+        } else {
+            return new HttpDownloader(url, context);
         }
-        return new HttpDownloader(url, context);
     }
 
-    public static Downloader create(String url, File destFile)
-            throws IOException {
-        if (isOnionAddress(url)) {
+    public static Downloader create(String url, File destFile) throws IOException {
+        Uri uri = Uri.parse(url);
+        if (isBluetoothAddress(uri)) {
+            return new BluetoothDownloader(null, uri.getPath(), destFile);
+        } else if (isOnionAddress(url)) {
             return new TorHttpDownloader(url, destFile);
+        } else {
+            return new HttpDownloader(url, destFile);
         }
-        return new HttpDownloader(url, destFile);
+    }
+
+    private static boolean isBluetoothAddress(Uri uri) {
+        return "bluetooth".equalsIgnoreCase(uri.getScheme());
     }
 
     private static boolean isOnionAddress(String url) {
diff --git a/bluetooth-notes.txt b/bluetooth-notes.txt
deleted file mode 100644
index 918d0ca13..000000000
--- a/bluetooth-notes.txt
+++ /dev/null
@@ -1,21 +0,0 @@
-One is server, the other is the client (always the case with Bluetooth).
-
-When does the pairing happen? I can think of a few times:
-
-Use case 1 - 
- * Swapper decides to use bluetooth to send apps to others.
- * Selects "Use bluetooth instead" on the "join wifi" screen.
- * Starts a bluetooth server 
-   + Make itself discoverable
-   + Opens a bluetooth server socket
-   + Waits for incoming client connections.
-
- * Swapee opens swap workflow
- * Selects the bluetooth option
- * Is asked to pair with nearby bluetooth devices, using the F-Droid UUID to make sure it doesn't connect to, e.g. bluetooth headphones.
- * Stays connected in the background
- * Adds the repo as per usual (with a url such as bluetooth://device-mac-address)
- * When repo updates, it uses the open connection to get data
- * If the connection has closed, attempts to reconnect
- * Same when downloading files
-

From 99a86c4080d0972393c5dd9092c2c9e2454b9e3a Mon Sep 17 00:00:00 2001
From: n8fr8 <nathan@freitas.net>
Date: Mon, 13 Jul 2015 14:40:11 -0400
Subject: [PATCH 10/16] handle null adapter case

---
 .../views/swap/SwapWorkflowActivity.java      | 26 ++++++++++---------
 1 file changed, 14 insertions(+), 12 deletions(-)

diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
index 075044e5e..432a6180f 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
@@ -296,15 +296,16 @@ public class SwapWorkflowActivity extends ActionBarActivity {
 
         Log.d(TAG, "Initiating Bluetooth swap instead of wifi.");
         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
-        // TODO: May be null (e.g. on an emulator).
-        if (adapter.isEnabled()) {
-            Log.d(TAG, "Bluetooth enabled, will pair with device.");
-            ensureBluetoothDiscoverable();
-        } else {
-            Log.d(TAG, "Bluetooth disabled, asking user to enable it.");
-            Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
-            startActivityForResult(enableBtIntent, REQUEST_BLUETOOTH_ENABLE);
-        }
+
+        if (adapter != null)
+            if (adapter.isEnabled()) {
+                Log.d(TAG, "Bluetooth enabled, will pair with device.");
+                ensureBluetoothDiscoverable();
+            } else {
+                Log.d(TAG, "Bluetooth disabled, asking user to enable it.");
+                Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
+                startActivityForResult(enableBtIntent, REQUEST_BLUETOOTH_ENABLE);
+            }
     }
 
     private void ensureBluetoothDiscoverable() {
@@ -318,10 +319,11 @@ public class SwapWorkflowActivity extends ActionBarActivity {
             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();
         }
+
+        Log.d(TAG, "Staring the Bluetooth Server whether we are discoverable or not, since paired devices can still connect.");
+        startBluetoothServer();
+
     }
 
     private void startBluetoothServer() {

From d662c15fb8c5c06e71657e03c65499011e93a561 Mon Sep 17 00:00:00 2001
From: n8fr8 <nathan@freitas.net>
Date: Mon, 13 Jul 2015 14:40:22 -0400
Subject: [PATCH 11/16] list background should be white

---
 F-Droid/res/layout/swap_bluetooth_devices.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/F-Droid/res/layout/swap_bluetooth_devices.xml b/F-Droid/res/layout/swap_bluetooth_devices.xml
index 746855ef7..fd16a39ed 100644
--- a/F-Droid/res/layout/swap_bluetooth_devices.xml
+++ b/F-Droid/res/layout/swap_bluetooth_devices.xml
@@ -5,6 +5,6 @@
         android:orientation="vertical"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:background="@color/swap_blue">
+        android:background="@color/white">
 
 </org.fdroid.fdroid.views.swap.BluetoothDeviceListView>
\ No newline at end of file

From cc6b2736cea697e4b170c0318e5518990b341a4b Mon Sep 17 00:00:00 2001
From: n8fr8 <nathan@freitas.net>
Date: Mon, 13 Jul 2015 14:41:21 -0400
Subject: [PATCH 12/16] add logic for setting/re-setting "Fdroid" name tag for
 bluetooth device

---
 .../fdroid/net/bluetooth/BluetoothServer.java   | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

diff --git a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
index 493cc47be..08b5264bf 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
@@ -29,6 +29,9 @@ public class BluetoothServer extends Thread {
 
     private final Context context;
 
+    private String deviceBluetoothName = null;
+    public final static String BLUETOOTH_NAME_TAG = "FDroid:";
+
     public BluetoothServer(Context context) {
         this.context = context.getApplicationContext();
     }
@@ -43,14 +46,26 @@ public class BluetoothServer extends Thread {
             Utils.closeQuietly(serverSocket);
         }
 
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        adapter.setName(deviceBluetoothName.replace(BLUETOOTH_NAME_TAG,""));
+
     }
 
     @Override
     public void run() {
 
         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+
+
+        //store the original bluetoothname, and update this one to be unique
+        deviceBluetoothName = adapter.getName();
+
+        if (!deviceBluetoothName.contains(BLUETOOTH_NAME_TAG))
+            adapter.setName(BLUETOOTH_NAME_TAG + deviceBluetoothName);
+
+
         try {
-            serverSocket = adapter.listenUsingRfcommWithServiceRecord("FDroid App Swap", BluetoothConstants.fdroidUuid());
+            serverSocket = adapter.listenUsingInsecureRfcommWithServiceRecord("FDroid App Swap", BluetoothConstants.fdroidUuid());
         } catch (IOException e) {
             Log.e(TAG, "Error starting Bluetooth server socket, will stop the server now - " + e.getMessage());
             return;

From c399a17369a56550835a9aff46141452778ac286 Mon Sep 17 00:00:00 2001
From: n8fr8 <nathan@freitas.net>
Date: Mon, 13 Jul 2015 15:04:38 -0400
Subject: [PATCH 13/16] add more checks for possible NPE and simplify the UI

---
 .../fdroid/net/bluetooth/BluetoothClient.java |  2 +-
 .../views/swap/BluetoothDeviceListView.java   | 39 ++++++++++++-------
 2 files changed, 25 insertions(+), 16 deletions(-)

diff --git a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
index fe45b8ee3..3179c259e 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
@@ -17,7 +17,7 @@ public class BluetoothClient {
     }
 
     public BluetoothConnection openConnection() throws IOException {
-        BluetoothSocket socket = device.createRfcommSocketToServiceRecord(BluetoothConstants.fdroidUuid());
+        BluetoothSocket socket = device.createInsecureRfcommSocketToServiceRecord(BluetoothConstants.fdroidUuid());
         BluetoothConnection connection = new BluetoothConnection(socket);
         connection.open();
         return connection;
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListView.java
index 00c4a48c5..66b1bd462 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListView.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListView.java
@@ -31,6 +31,7 @@ import org.fdroid.fdroid.localrepo.SwapManager;
 import org.fdroid.fdroid.net.BluetoothDownloader;
 import org.fdroid.fdroid.net.bluetooth.BluetoothClient;
 import org.fdroid.fdroid.net.bluetooth.BluetoothConnection;
+import org.fdroid.fdroid.net.bluetooth.BluetoothServer;
 import org.fdroid.fdroid.views.fragments.ThemeableListFragment;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -134,13 +135,15 @@ public class BluetoothDeviceListView extends ListView implements
 
         final BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
 
-        final TextView deviceName = (TextView)headerView.findViewById(R.id.device_name);
+        final TextView deviceName = (TextView) headerView.findViewById(R.id.device_name);
         deviceName.setText(bluetooth.getName());
 
-        final TextView address = (TextView)headerView.findViewById(R.id.device_address);
+        final TextView address = (TextView) headerView.findViewById(R.id.device_address);
         address.setText(bluetooth.getAddress());
 
-        populateBondedDevices();
+        initiateBluetoothScan();
+
+       // populateBondedDevices();
 
     }
 
@@ -166,8 +169,10 @@ public class BluetoothDeviceListView extends ListView implements
     {
         Log.d(TAG, "Starting bluetooth scan...");
 
-        cancelMenuItem.setVisible(true);
-        scanMenuItem.setVisible(false);
+        if (cancelMenuItem != null) {
+            cancelMenuItem.setVisible(true);
+            scanMenuItem.setVisible(false);
+        }
 
         final ContentLoadingProgressBar loadingBar = getLoadingIndicator();
 
@@ -181,17 +186,21 @@ public class BluetoothDeviceListView extends ListView implements
                 if (BluetoothDevice.ACTION_FOUND.equals(intent.getAction())) {
                     BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                     Log.d(TAG, "Found bluetooth device: " + device.toString());
-                    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);
-                    }
+                    if (device != null && device.getName() != null)
+                        if (device.getName().contains(BluetoothServer.BLUETOOTH_NAME_TAG)) {
+                            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);
+                            }
+                        }
                 }
             }
         };

From c53df989b80c0fbbf2bd8742234a20cd33f68f1e Mon Sep 17 00:00:00 2001
From: n8fr8 <nathan@freitas.net>
Date: Mon, 13 Jul 2015 16:05:00 -0400
Subject: [PATCH 14/16] small UI clean-up of header display

---
 F-Droid/res/layout/swap_bluetooth_header.xml | 25 ++++++++++++++------
 1 file changed, 18 insertions(+), 7 deletions(-)

diff --git a/F-Droid/res/layout/swap_bluetooth_header.xml b/F-Droid/res/layout/swap_bluetooth_header.xml
index 89992d1ae..e74261ef9 100644
--- a/F-Droid/res/layout/swap_bluetooth_header.xml
+++ b/F-Droid/res/layout/swap_bluetooth_header.xml
@@ -6,7 +6,9 @@
     android:orientation="vertical"
     android:gravity="center"
     android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
+    android:layout_height="wrap_content"
+    android:background="@color/fdroid_blue"
+    >
 
     <TextView
             android:layout_width="wrap_content"
@@ -16,8 +18,10 @@
             android:gravity="center_horizontal"
             style="@style/SwapTheme.BluetoothDeviceList.Heading"
             android:paddingTop="10dp"
-            android:paddingBottom="10dp"
-            android:textSize="24sp"/>
+            android:paddingBottom="5dp"
+            android:textSize="24sp"
+            android:textColor="@color/white"
+        />
 
     <TextView
             android:layout_width="wrap_content"
@@ -26,7 +30,9 @@
             tools:text="Phone v2.0"
             android:gravity="center_horizontal"
             style="@style/SwapTheme.BluetoothDeviceList.Heading"
-            android:paddingBottom="10dp"/>
+            android:paddingBottom="5dp"
+            android:textColor="@color/white"
+        />
 
     <TextView
             android:layout_width="wrap_content"
@@ -35,8 +41,10 @@
             tools:text="(01:02:03:ab:cd:ef)"
             android:gravity="center_horizontal"
             style="@style/SwapTheme.BluetoothDeviceList.Heading"
-            android:paddingBottom="20dp"
-            android:textSize="24sp"/>
+            android:paddingBottom="10dp"
+            android:textSize="18sp"
+            android:textColor="@color/white"
+        />
 
     <TextView
             android:layout_width="wrap_content"
@@ -46,7 +54,10 @@
             style="@style/SwapTheme.BluetoothDeviceList.Text"
             android:paddingLeft="20dp"
             android:paddingRight="20dp"
-            android:paddingBottom="10dp"/>
+            android:paddingBottom="5dp"
+            android:textColor="@color/white"
+
+        />
 
     <android.support.v4.widget.ContentLoadingProgressBar
             android:layout_width="match_parent"

From e930e0337899eaa01954ade553ced0aa6690b610 Mon Sep 17 00:00:00 2001
From: n8fr8 <nathan@freitas.net>
Date: Mon, 13 Jul 2015 16:05:13 -0400
Subject: [PATCH 15/16] improvement on scan/re-scan logic to avoid failure

---
 .../views/swap/BluetoothDeviceListView.java   | 90 ++++++++++++-------
 1 file changed, 56 insertions(+), 34 deletions(-)

diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListView.java
index 66b1bd462..f01a30ac1 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListView.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListView.java
@@ -26,6 +26,8 @@ import android.widget.AdapterView;
 import android.widget.ArrayAdapter;
 import android.widget.ListView;
 import android.widget.TextView;
+import android.widget.Toast;
+
 import org.fdroid.fdroid.R;
 import org.fdroid.fdroid.localrepo.SwapManager;
 import org.fdroid.fdroid.net.BluetoothDownloader;
@@ -48,6 +50,8 @@ public class BluetoothDeviceListView extends ListView implements
     private MenuItem scanMenuItem;
     private MenuItem cancelMenuItem;
 
+    private boolean firstScan = true;
+
     public BluetoothDeviceListView(Context context) {
         super(context);
     }
@@ -132,6 +136,7 @@ public class BluetoothDeviceListView extends ListView implements
         addHeaderView(headerView);
 
         setAdapter(adapter);
+        setOnItemClickListener(this);
 
         final BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
 
@@ -147,6 +152,8 @@ public class BluetoothDeviceListView extends ListView implements
 
     }
 
+
+
     private void cancelBluetoothScan() {
 
         Log.d(TAG, "Cancelling bluetooth scan.");
@@ -178,49 +185,62 @@ public class BluetoothDeviceListView extends ListView implements
 
         loadingBar.show();
 
-
         final BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
-        final 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);
-                    Log.d(TAG, "Found bluetooth device: " + device.toString());
 
-                    if (device != null && device.getName() != null)
-                        if (device.getName().contains(BluetoothServer.BLUETOOTH_NAME_TAG)) {
-                            boolean exists = false;
-                            for (int i = 0; i < adapter.getCount(); i++) {
-                                if (adapter.getItem(i).getAddress().equals(device.getAddress())) {
-                                    exists = true;
-                                    break;
+        if (firstScan) {
+            final 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);
+                        Log.d(TAG, "Found bluetooth device: " + device.toString());
+
+                        if (device != null && device.getName() != null)
+                            if (device.getName().contains(BluetoothServer.BLUETOOTH_NAME_TAG)) {
+                                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);
                                 }
                             }
-
-                            if (!exists) {
-                                adapter.add(device);
-                            }
-                        }
+                    }
                 }
-            }
-        };
+            };
 
-        final BroadcastReceiver scanCompleteReceiver = new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                Log.d(TAG, "Scan complete: " + intent.getAction());
-                loadingBar.hide();
-                cancelMenuItem.setVisible(false);
-                scanMenuItem.setVisible(true);
-            }
-        };
+            final BroadcastReceiver scanCompleteReceiver = new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    Log.d(TAG, "Scan complete: " + intent.getAction());
+                    loadingBar.hide();
+                    cancelMenuItem.setVisible(false);
+                    scanMenuItem.setVisible(true);
+                }
+            };
 
-        getContext().registerReceiver(deviceFoundReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND));
-        getContext().registerReceiver(scanCompleteReceiver, new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED));
+            getContext().registerReceiver(deviceFoundReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND));
+            getContext().registerReceiver(scanCompleteReceiver, new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED));
+
+            firstScan = false;
+        }
+        else
+        {
+            if (bluetooth.isDiscovering())
+            {
+                bluetooth.cancelDiscovery();
+            }
+        }
 
         if (!bluetooth.startDiscovery()) {
             // TODO: Discovery did not start for some reason :(
             Log.e(TAG, "Could not start bluetooth discovery, but am not sure why :(");
+            Toast.makeText(getContext(),"There was a problem looking for Bluetooth devices",Toast.LENGTH_SHORT).show();
         }
     }
 
@@ -231,6 +251,8 @@ public class BluetoothDeviceListView extends ListView implements
         }
     }
 
+
+
     @Override
     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
 
@@ -327,11 +349,11 @@ public class BluetoothDeviceListView extends ListView implements
             BluetoothDevice device = getItem(position);
             TextView nameView = (TextView)view.findViewById(android.R.id.text1);
             TextView addressView = (TextView)view.findViewById(android.R.id.text2);
-            TextView descriptionView = (TextView)view.findViewById(R.id.text3);
+            //TextView descriptionView = (TextView)view.findViewById(R.id.text3);
 
             nameView.setText(device.getName() == null ? getContext().getString(R.string.unknown) : device.getName());
             addressView.setText(device.getAddress());
-            descriptionView.setText(bondStateToLabel(device.getBondState()));
+            //descriptionView.setText(bondStateToLabel(device.getBondState()));
 
             return view;
         }

From 2a6b5142324fca408760807a7db926fbdd635c8b Mon Sep 17 00:00:00 2001
From: n8fr8 <nathan@freitas.net>
Date: Mon, 13 Jul 2015 16:45:53 -0400
Subject: [PATCH 16/16] change BT request handling to operate directly on files
 We don't want to depend on HTTP being active in case there is no wifi that
 exists at all. In some cases, local 127.0.0.1 does not exist if the Wifi is
 not connected. This commit takes the code from LocalHTTPD and repurposes it
 for the BluetoothServer needs.

---
 .../fdroid/net/bluetooth/BluetoothServer.java | 229 +++++++++++++++++-
 .../net/bluetooth/httpish/Response.java       |  18 ++
 .../views/swap/SwapWorkflowActivity.java      |   2 +-
 3 files changed, 235 insertions(+), 14 deletions(-)

diff --git a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
index 08b5264bf..9066ba5f5 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
@@ -6,15 +6,27 @@ import android.bluetooth.BluetoothSocket;
 import android.content.Context;
 import android.os.Build;
 import android.util.Log;
+import android.webkit.MimeTypeMap;
+
 import org.fdroid.fdroid.FDroidApp;
 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.File;
+import java.io.FileInputStream;
+import java.io.FilenameFilter;
 import java.io.IOException;
+import java.io.InputStream;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+
+import fi.iki.elonen.NanoHTTPD;
 
 /**
  * Act as a layer on top of LocalHTTPD server, by forwarding requests served
@@ -31,9 +43,11 @@ public class BluetoothServer extends Thread {
 
     private String deviceBluetoothName = null;
     public final static String BLUETOOTH_NAME_TAG = "FDroid:";
+    private final File webRoot;
 
-    public BluetoothServer(Context context) {
+    public BluetoothServer(Context context, File webRoot) {
         this.context = context.getApplicationContext();
+        this.webRoot = webRoot;
     }
 
     public void close() {
@@ -75,7 +89,7 @@ public class BluetoothServer extends Thread {
             try {
                 BluetoothSocket clientSocket = serverSocket.accept();
                 if (clientSocket != null && !isInterrupted()) {
-                    Connection client = new Connection(context, clientSocket);
+                    Connection client = new Connection(context, clientSocket, webRoot);
                     client.start();
                     clients.add(client);
                 } else {
@@ -88,15 +102,16 @@ public class BluetoothServer extends Thread {
 
     }
 
-    private static class Connection extends Thread
-    {
+    private static class Connection extends Thread {
 
         private final Context context;
         private final BluetoothSocket socket;
+        private final File webRoot;
 
-        public Connection(Context context, BluetoothSocket socket) {
+        public Connection(Context context, BluetoothSocket socket, File webRoot) {
             this.context = context.getApplicationContext();
             this.socket = socket;
+            this.webRoot = webRoot;
         }
 
         @Override
@@ -121,6 +136,8 @@ public class BluetoothServer extends Thread {
                     handleRequest(incomingRequest).send(connection);
                 } catch (IOException e) {
                     Log.e(TAG, "Error receiving incoming connection over bluetooth - " + e.getMessage());
+
+
                 }
 
                 if (isInterrupted())
@@ -134,34 +151,220 @@ public class BluetoothServer extends Thread {
 
             Log.d(TAG, "Received Bluetooth request from client, will process it now.");
 
-            try {
-                HttpDownloader downloader = new HttpDownloader("http://127.0.0.1:" + ( FDroidApp.port + 1 ) + "/" + request.getPath(), context);
+            Response.Builder builder = null;
 
-                Response.Builder builder;
+            try {
+//                HttpDownloader downloader = new HttpDownloader("http://127.0.0.1:" + ( FDroidApp.port) + "/" + request.getPath(), context);
+                int statusCode = 404;
+                int totalSize = -1;
 
                 if (request.getMethod().equals(Request.Methods.HEAD)) {
                     builder = new Response.Builder();
                 } else {
-                    builder = new Response.Builder(downloader.getInputStream());
+                    HashMap<String, String> headers = new HashMap<String, String>();
+                    Response resp = respond(headers, "/" + request.getPath());
+
+                    builder = new Response.Builder(resp.toContentStream());
+                    statusCode = resp.getStatusCode();
+                    totalSize = resp.getFileSize();
                 }
 
                 // 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(downloader.getStatusCode())
-                        .setFileSize(downloader.totalDownloadSize())
+                        .setStatusCode(statusCode)
+                        .setFileSize(totalSize)
                         .build();
 
-            } catch (IOException e) {
+            } catch (Exception e) {
+                /*
                 if (Build.VERSION.SDK_INT <= 9) {
                     // Would like to use the specific IOException below with a "cause", but it is
                     // only supported on SDK 9, so I guess this is the next most useful thing.
                     throw e;
                 } else {
                     throw new IOException("Error getting file " + request.getPath() + " from local repo proxy - " + e.getMessage(), e);
-                }
+                }*/
+
+                Log.e(TAG, "error processing request; sending 500 response", e);
+
+                if (builder == null)
+                    builder = new Response.Builder();
+
+                return builder
+                        .setStatusCode(500)
+                        .setFileSize(0)
+                        .build();
+
             }
 
         }
+
+
+        private Response respond(Map<String, String> headers, String uri) {
+            // Remove URL arguments
+            uri = uri.trim().replace(File.separatorChar, '/');
+            if (uri.indexOf('?') >= 0) {
+                uri = uri.substring(0, uri.indexOf('?'));
+            }
+
+            // Prohibit getting out of current directory
+            if (uri.contains("../")) {
+                return createResponse(NanoHTTPD.Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT,
+                        "FORBIDDEN: Won't serve ../ for security reasons.");
+            }
+
+            File f = new File(webRoot, uri);
+            if (!f.exists()) {
+                return createResponse(NanoHTTPD.Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT,
+                        "Error 404, file not found.");
+            }
+
+            // Browsers get confused without '/' after the directory, send a
+            // redirect.
+            if (f.isDirectory() && !uri.endsWith("/")) {
+                uri += "/";
+                Response res = createResponse(NanoHTTPD.Response.Status.REDIRECT, NanoHTTPD.MIME_HTML,
+                        "<html><body>Redirected: <a href=\"" +
+                                uri + "\">" + uri + "</a></body></html>");
+                res.addHeader("Location", uri);
+                return res;
+            }
+
+            if (f.isDirectory()) {
+                // First look for index files (index.html, index.htm, etc) and if
+                // none found, list the directory if readable.
+                String indexFile = findIndexFileInDirectory(f);
+                if (indexFile == null) {
+                    if (f.canRead()) {
+                        // No index file, list the directory if it is readable
+                        return createResponse(NanoHTTPD.Response.Status.NOT_FOUND, NanoHTTPD.MIME_HTML, "");
+                    } else {
+                        return createResponse(NanoHTTPD.Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT,
+                                "FORBIDDEN: No directory listing.");
+                    }
+                } else {
+                    return respond(headers, uri + indexFile);
+                }
+            }
+
+            Response response = serveFile(uri, headers, f, getMimeTypeForFile(uri));
+            return response != null ? response :
+                    createResponse(NanoHTTPD.Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT,
+                            "Error 404, file not found.");
+        }
+
+        /**
+         * Serves file from homeDir and its' subdirectories (only). Uses only URI,
+         * ignores all headers and HTTP parameters.
+         */
+        Response serveFile(String uri, Map<String, String> header, File file, String mime) {
+            Response res;
+            try {
+                // Calculate etag
+                String etag = Integer
+                        .toHexString((file.getAbsolutePath() + file.lastModified() + "" + file.length())
+                                .hashCode());
+
+                // Support (simple) skipping:
+                long startFrom = 0;
+                long endAt = -1;
+                String range = header.get("range");
+                if (range != null) {
+                    if (range.startsWith("bytes=")) {
+                        range = range.substring("bytes=".length());
+                        int minus = range.indexOf('-');
+                        try {
+                            if (minus > 0) {
+                                startFrom = Long.parseLong(range.substring(0, minus));
+                                endAt = Long.parseLong(range.substring(minus + 1));
+                            }
+                        } catch (NumberFormatException ignored) {
+                        }
+                    }
+                }
+
+                // Change return code and add Content-Range header when skipping is
+                // requested
+                long fileLen = file.length();
+                if (range != null && startFrom >= 0) {
+                    if (startFrom >= fileLen) {
+                        res = createResponse(NanoHTTPD.Response.Status.RANGE_NOT_SATISFIABLE,
+                                NanoHTTPD.MIME_PLAINTEXT, "");
+                        res.addHeader("Content-Range", "bytes 0-0/" + fileLen);
+                        res.addHeader("ETag", etag);
+                    } else {
+                        if (endAt < 0) {
+                            endAt = fileLen - 1;
+                        }
+                        long newLen = endAt - startFrom + 1;
+                        if (newLen < 0) {
+                            newLen = 0;
+                        }
+
+                        final long dataLen = newLen;
+                        FileInputStream fis = new FileInputStream(file) {
+                            @Override
+                            public int available() throws IOException {
+                                return (int) dataLen;
+                            }
+                        };
+                        fis.skip(startFrom);
+
+                        res = createResponse(NanoHTTPD.Response.Status.PARTIAL_CONTENT, mime, fis);
+                        res.addHeader("Content-Length", "" + dataLen);
+                        res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/"
+                                + fileLen);
+                        res.addHeader("ETag", etag);
+                    }
+                } else {
+                    if (etag.equals(header.get("if-none-match")))
+                        res = createResponse(NanoHTTPD.Response.Status.NOT_MODIFIED, mime, "");
+                    else {
+                        res = createResponse(NanoHTTPD.Response.Status.OK, mime, new FileInputStream(file));
+                        res.addHeader("Content-Length", "" + fileLen);
+                        res.addHeader("ETag", etag);
+                    }
+                }
+            } catch (IOException ioe) {
+                res = createResponse(NanoHTTPD.Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT,
+                        "FORBIDDEN: Reading file failed.");
+            }
+
+            return res;
+        }
+
+        // Announce that the file server accepts partial content requests
+        private Response createResponse(NanoHTTPD.Response.Status status, String mimeType, String content) {
+            Response res = new Response(status.getRequestStatus(), mimeType, content);
+            return res;
+        }
+
+        // Announce that the file server accepts partial content requests
+        private Response createResponse(NanoHTTPD.Response.Status status, String mimeType, InputStream content) {
+            Response res = new Response(status.getRequestStatus(), mimeType, content);
+            return res;
+        }
+
+        public static String getMimeTypeForFile(String uri) {
+            String type = null;
+            String extension = MimeTypeMap.getFileExtensionFromUrl(uri);
+            if (extension != null) {
+                MimeTypeMap mime = MimeTypeMap.getSingleton();
+                type = mime.getMimeTypeFromExtension(extension);
+            }
+            return type;
+        }
+
+        private String findIndexFileInDirectory(File directory) {
+            String indexFileName = "index.html";
+            File indexFile = new File(directory, indexFileName);
+            if (indexFile.exists()) {
+                return indexFileName;
+            }
+            return null;
+        }
     }
+
+
 }
diff --git a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java
index 3e5b274b3..1f1fc5ad5 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java
@@ -10,6 +10,7 @@ import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStreamWriter;
+import java.io.StringBufferInputStream;
 import java.io.Writer;
 import java.util.HashMap;
 import java.util.Map;
@@ -38,6 +39,23 @@ public class Response {
         this.contentStream = contentStream;
     }
 
+    public Response(int statusCode, String mimeType, String content) {
+        this.statusCode = statusCode;
+        this.headers = new HashMap<String,String>();
+        this.contentStream = new StringBufferInputStream(content);
+    }
+
+    public Response(int statusCode, String mimeType, InputStream contentStream) {
+        this.statusCode = statusCode;
+        this.headers = new HashMap<String,String>();
+        this.contentStream = contentStream;
+    }
+
+    public void addHeader (String key, String value)
+    {
+        headers.put(key, value);
+    }
+
     public int getStatusCode() {
         return statusCode;
     }
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
index 432a6180f..f61702c51 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
@@ -331,7 +331,7 @@ public class SwapWorkflowActivity extends ActionBarActivity {
         if (!state.isEnabled()) {
             state.enableSwapping();
         }
-        new BluetoothServer(this).start();
+        new BluetoothServer(this,getFilesDir()).start();
         showBluetoothDeviceList();
     }