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:
parent
239ccbf0f3
commit
45a3efa2b3
81
src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
Normal file
81
src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
14
src/org/fdroid/fdroid/net/bluetooth/BluetoothConstants.java
Normal file
14
src/org/fdroid/fdroid/net/bluetooth/BluetoothConstants.java
Normal 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";
|
||||
}
|
||||
|
||||
}
|
94
src/org/fdroid/fdroid/net/bluetooth/BluetoothDownloader.java
Normal file
94
src/org/fdroid/fdroid/net/bluetooth/BluetoothDownloader.java
Normal 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())
|
||||
);
|
||||
}
|
||||
|
||||
}
|
23
src/org/fdroid/fdroid/net/bluetooth/FileDetails.java
Normal file
23
src/org/fdroid/fdroid/net/bluetooth/FileDetails.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
90
src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java
Normal file
90
src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
56
src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java
Normal file
56
src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user