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