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.
This commit is contained in:
Peter Serwylo 2014-04-12 08:09:14 +00:00 committed by Peter Serwylo
parent 239ccbf0f3
commit 45a3efa2b3
10 changed files with 427 additions and 0 deletions

View File

@ -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();
}
}
}

View File

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

View File

@ -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())
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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