diff --git a/F-Droid/src/org/fdroid/fdroid/net/Downloader.java b/F-Droid/src/org/fdroid/fdroid/net/Downloader.java index 746fcc1c7..af1c6325c 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/Downloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/Downloader.java @@ -127,7 +127,8 @@ public abstract class Downloader { // we were interrupted before proceeding to the download. throwExceptionIfInterrupted(); - copyInputToOutputStream(getInputStream()); + // TODO: Check side effects of changing this second getInputStream() to input. + copyInputToOutputStream(input); } finally { Utils.closeQuietly(outputStream); Utils.closeQuietly(input); @@ -173,12 +174,13 @@ public abstract class Downloader { int count = input.read(buffer); throwExceptionIfInterrupted(); - bytesRead += count; - sendProgress(bytesRead, totalBytes); if (count == -1) { Log.d(TAG, "Finished downloading from stream"); break; } + + bytesRead += count; + sendProgress(bytesRead, totalBytes); outputStream.write(buffer, 0, count); } outputStream.flush(); diff --git a/src/org/apache/commons/io/input/BoundedInputStream.java b/src/org/apache/commons/io/input/BoundedInputStream.java new file mode 100644 index 000000000..f80c1730f --- /dev/null +++ b/src/org/apache/commons/io/input/BoundedInputStream.java @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.input; + +import java.io.IOException; +import java.io.InputStream; + +/** + * This is a stream that will only supply bytes up to a certain length - if its + * position goes above that, it will stop. + *

+ * This is useful to wrap ServletInputStreams. The ServletInputStream will block + * if you try to read content from it that isn't there, because it doesn't know + * whether the content hasn't arrived yet or whether the content has finished. + * So, one of these, initialized with the Content-length sent in the + * ServletInputStream's header, will stop it blocking, providing it's been sent + * with a correct content length. + * + * @version $Id: BoundedInputStream.java 1307462 2012-03-30 15:13:11Z ggregory $ + * @since 2.0 + */ +public class BoundedInputStream extends InputStream { + + /** the wrapped input stream */ + private final InputStream in; + + /** the max length to provide */ + private final long max; + + /** the number of bytes already returned */ + private long pos = 0; + + /** the marked position */ + private long mark = -1; + + /** flag if close shoud be propagated */ + private boolean propagateClose = true; + + /** + * Creates a new BoundedInputStream that wraps the given input + * stream and limits it to a certain size. + * + * @param in The wrapped input stream + * @param size The maximum number of bytes to return + */ + public BoundedInputStream(InputStream in, long size) { + // Some badly designed methods - eg the servlet API - overload length + // such that "-1" means stream finished + this.max = size; + this.in = in; + } + + /** + * Creates a new BoundedInputStream that wraps the given input + * stream and is unlimited. + * + * @param in The wrapped input stream + */ + public BoundedInputStream(InputStream in) { + this(in, -1); + } + + /** + * Invokes the delegate's read() method if + * the current position is less than the limit. + * @return the byte read or -1 if the end of stream or + * the limit has been reached. + * @throws IOException if an I/O error occurs + */ + @Override + public int read() throws IOException { + if (max >= 0 && pos >= max) { + return -1; + } + int result = in.read(); + pos++; + return result; + } + + /** + * Invokes the delegate's read(byte[]) method. + * @param b the buffer to read the bytes into + * @return the number of bytes read or -1 if the end of stream or + * the limit has been reached. + * @throws IOException if an I/O error occurs + */ + @Override + public int read(byte[] b) throws IOException { + return this.read(b, 0, b.length); + } + + /** + * Invokes the delegate's read(byte[], int, int) method. + * @param b the buffer to read the bytes into + * @param off The start offset + * @param len The number of bytes to read + * @return the number of bytes read or -1 if the end of stream or + * the limit has been reached. + * @throws IOException if an I/O error occurs + */ + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (max>=0 && pos>=max) { + return -1; + } + long maxRead = max>=0 ? Math.min(len, max-pos) : len; + int bytesRead = in.read(b, off, (int)maxRead); + + if (bytesRead==-1) { + return -1; + } + + pos+=bytesRead; + return bytesRead; + } + + /** + * Invokes the delegate's skip(long) method. + * @param n the number of bytes to skip + * @return the actual number of bytes skipped + * @throws IOException if an I/O error occurs + */ + @Override + public long skip(long n) throws IOException { + long toSkip = max>=0 ? Math.min(n, max-pos) : n; + long skippedBytes = in.skip(toSkip); + pos+=skippedBytes; + return skippedBytes; + } + + /** + * {@inheritDoc} + */ + @Override + public int available() throws IOException { + if (max>=0 && pos>=max) { + return 0; + } + return in.available(); + } + + /** + * Invokes the delegate's toString() method. + * @return the delegate's toString() + */ + @Override + public String toString() { + return in.toString(); + } + + /** + * Invokes the delegate's close() method + * if {@link #isPropagateClose()} is {@code true}. + * @throws IOException if an I/O error occurs + */ + @Override + public void close() throws IOException { + if (propagateClose) { + in.close(); + } + } + + /** + * Invokes the delegate's reset() method. + * @throws IOException if an I/O error occurs + */ + @Override + public synchronized void reset() throws IOException { + in.reset(); + pos = mark; + } + + /** + * Invokes the delegate's mark(int) method. + * @param readlimit read ahead limit + */ + @Override + public synchronized void mark(int readlimit) { + in.mark(readlimit); + mark = pos; + } + + /** + * Invokes the delegate's markSupported() method. + * @return true if mark is supported, otherwise false + */ + @Override + public boolean markSupported() { + return in.markSupported(); + } + + /** + * Indicates whether the {@link #close()} method + * should propagate to the underling {@link InputStream}. + * + * @return {@code true} if calling {@link #close()} + * propagates to the close() method of the + * underlying stream or {@code false} if it does not. + */ + public boolean isPropagateClose() { + return propagateClose; + } + + /** + * Set whether the {@link #close()} method + * should propagate to the underling {@link InputStream}. + * + * @param propagateClose {@code true} if calling + * {@link #close()} propagates to the close() + * method of the underlying stream or + * {@code false} if it does not. + */ + public void setPropagateClose(boolean propagateClose) { + this.propagateClose = propagateClose; + } +} diff --git a/src/org/fdroid/fdroid/net/BluetoothDownloader.java b/src/org/fdroid/fdroid/net/BluetoothDownloader.java index c1c770056..5dcf02758 100644 --- a/src/org/fdroid/fdroid/net/BluetoothDownloader.java +++ b/src/org/fdroid/fdroid/net/BluetoothDownloader.java @@ -2,7 +2,8 @@ package org.fdroid.fdroid.net; import android.content.Context; import android.util.Log; -import org.fdroid.fdroid.net.bluetooth.BluetoothClient; +import org.apache.commons.io.input.BoundedInputStream; +import org.fdroid.fdroid.net.bluetooth.BluetoothConnection; import org.fdroid.fdroid.net.bluetooth.FileDetails; import org.fdroid.fdroid.net.bluetooth.httpish.Request; import org.fdroid.fdroid.net.bluetooth.httpish.Response; @@ -18,30 +19,50 @@ public class BluetoothDownloader extends Downloader { private static final String TAG = "org.fdroid.fdroid.net.BluetoothDownloader"; - private BluetoothClient client; + private final BluetoothConnection connection; private FileDetails fileDetails; + private final String sourcePath; - BluetoothDownloader(BluetoothClient client, String destFile, Context ctx) throws FileNotFoundException, MalformedURLException { + public BluetoothDownloader(BluetoothConnection connection, String sourcePath, String destFile, Context ctx) throws FileNotFoundException, MalformedURLException { super(destFile, ctx); + this.connection = connection; + this.sourcePath = sourcePath; } - BluetoothDownloader(BluetoothClient client, Context ctx) throws IOException { + public BluetoothDownloader(BluetoothConnection connection, String sourcePath, Context ctx) throws IOException { super(ctx); + this.connection = connection; + this.sourcePath = sourcePath; } - BluetoothDownloader(BluetoothClient client, File destFile) throws FileNotFoundException, MalformedURLException { + public BluetoothDownloader(BluetoothConnection connection, String sourcePath, File destFile) throws FileNotFoundException, MalformedURLException { super(destFile); + this.connection = connection; + this.sourcePath = sourcePath; } - BluetoothDownloader(BluetoothClient client, OutputStream output) throws MalformedURLException { + public BluetoothDownloader(BluetoothConnection connection, String sourcePath, OutputStream output) throws MalformedURLException { super(output); + this.connection = connection; + this.sourcePath = sourcePath; } @Override public InputStream getInputStream() throws IOException { - Response response = Request.createGET(sourceUrl.getPath(), client.openConnection()).send(); + Response response = Request.createGET(sourcePath, connection).send(); fileDetails = response.toFileDetails(); - return response.toContentStream(); + + // TODO: Manage the dependency which includes this class better? + // Right now, I only needed the one class from apache commons. + // There are countless classes online which provide this functionaligy, + // including some which are available from the Android SDK - the only + // problem is that they have a funky API which doesn't just wrap a + // plain old InputStream (the class is ContentLengthInputStream - + // whereas this BoundedInputStream is much more generic and useful + // to us). + BoundedInputStream stream = new BoundedInputStream(response.toContentStream(), fileDetails.getFileSize()); + stream.setPropagateClose(false); + return stream; } /** @@ -54,7 +75,7 @@ public class BluetoothDownloader extends Downloader { if (fileDetails == null) { Log.d(TAG, "Going to Bluetooth \"server\" to get file details."); try { - fileDetails = Request.createHEAD(sourceUrl.getPath(), client.openConnection()).send().toFileDetails(); + fileDetails = Request.createHEAD(sourceUrl.getPath(), connection).send().toFileDetails(); } catch (IOException e) { Log.e(TAG, "Error getting file details from Bluetooth \"server\": " + e.getMessage()); } @@ -64,7 +85,7 @@ public class BluetoothDownloader extends Downloader { @Override public boolean hasChanged() { - return getFileDetails().getCacheTag().equals(getCacheTag()); + return getFileDetails().getCacheTag() == null || getFileDetails().getCacheTag().equals(getCacheTag()); } @Override diff --git a/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java b/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java index 5affbf4e6..dcd0ccdaf 100644 --- a/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java +++ b/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java @@ -18,12 +18,12 @@ import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; import org.fdroid.fdroid.R; +import org.fdroid.fdroid.net.BluetoothDownloader; import org.fdroid.fdroid.net.bluetooth.BluetoothClient; import org.fdroid.fdroid.net.bluetooth.BluetoothConnection; -import org.fdroid.fdroid.net.bluetooth.httpish.Request; -import org.fdroid.fdroid.net.bluetooth.httpish.Response; import org.fdroid.fdroid.views.fragments.ThemeableListFragment; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; @@ -132,13 +132,28 @@ public class BluetoothDeviceListFragment extends ThemeableListFragment { try { Log.d(TAG, "Testing bluetooth connection (opening connection first)."); BluetoothConnection connection = client.openConnection(); - Log.d(TAG, "Creating HEAD request for resource at \"/\"..."); + + ByteArrayOutputStream stream = new ByteArrayOutputStream(4096); + BluetoothDownloader downloader = new BluetoothDownloader(connection, "/", stream); + downloader.downloadUninterrupted(); + String result = stream.toString(); + Log.d(TAG, "Download complete."); + Log.d(TAG, result); + + Log.d(TAG, "Downloading again..."); + downloader = new BluetoothDownloader(connection, "/fdroid/repo/index.xml", stream); + downloader.downloadUninterrupted(); + result = stream.toString(); + Log.d(TAG, "Download complete."); + Log.d(TAG, result); + + /*Log.d(TAG, "Creating HEAD request for resource at \"/\"..."); Request head = Request.createGET("/", connection); Log.d(TAG, "Sending request..."); Response response = head.send(); Log.d(TAG, "Response from bluetooth: " + response.getStatusCode()); String contents = response.readContents(); - Log.d(TAG, contents); + Log.d(TAG, contents);*/ } catch (IOException e) { Log.e(TAG, "Error: " + e.getMessage()); }