From 45a3efa2b37208be416ce7866f36a99a493ec539 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Sat, 12 Apr 2014 08:09:14 +0000 Subject: [PATCH] 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; + } + } + } + +}