From 45a3efa2b37208be416ce7866f36a99a493ec539 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Sat, 12 Apr 2014 08:09:14 +0000 Subject: [PATCH 1/9] 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 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 readHeaders() throws IOException { + Map headers = new HashMap(); + 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 headers; + private final InputStream contentStream; + + public Response(int statusCode, Map 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 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 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 Date: Wed, 8 Oct 2014 07:41:04 +1030 Subject: [PATCH 2/9] 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 @@ + 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" /> - + #222 + + 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 { @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 @@ + + + + + + + + + + + \ 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 clients = new ArrayList(); + + 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 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 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 { + + 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 objects) { + super(context, resource, objects); + } + + public Adapter(Context context, int resource, int textViewResourceId, List 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 Date: Tue, 14 Oct 2014 07:02:19 +1030 Subject: [PATCH 3/9] 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 @@ - + + 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 @@ @color/white - + + - + + - -