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