Request a small chunk instead of HEAD for images of unknown size.

This commit is contained in:
Greyson Parrelli 2019-04-11 13:15:47 -04:00
parent 29cdb5290b
commit f3f6cc87d9
2 changed files with 86 additions and 27 deletions

View File

@ -9,11 +9,13 @@ import com.bumptech.glide.util.ContentLengthInputStream;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import java.io.FilterInputStream; import java.io.FilterInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.security.SecureRandom;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -41,23 +43,31 @@ public class ChunkedDataFetcher {
public RequestController fetch(@NonNull String url, long contentLength, @NonNull Callback callback) { public RequestController fetch(@NonNull String url, long contentLength, @NonNull Callback callback) {
if (contentLength <= 0) { if (contentLength <= 0) {
return fetch(url, callback); return fetchChunksWithUnknownTotalSize(url, callback);
} }
CompositeRequestController compositeController = new CompositeRequestController(); CompositeRequestController compositeController = new CompositeRequestController();
fetchChunks(url, contentLength, compositeController, callback); fetchChunks(url, contentLength, Optional.absent(), compositeController, callback);
return compositeController; return compositeController;
} }
public RequestController fetch(@NonNull String url, @NonNull Callback callback) { private RequestController fetchChunksWithUnknownTotalSize(@NonNull String url, @NonNull Callback callback) {
CompositeRequestController compositeController = new CompositeRequestController(); CompositeRequestController compositeController = new CompositeRequestController();
Call headCall = client.newCall(new Request.Builder().url(url).head().cacheControl(NO_CACHE).build()); long chunkSize = new SecureRandom().nextInt(1024) + 1024;
compositeController.addController(new CallRequestController(headCall)); Request request = new Request.Builder()
.url(url)
.cacheControl(NO_CACHE)
.addHeader("Range", "bytes=0-" + (chunkSize - 1))
.addHeader("Accept-Encoding", "identity")
.build();
headCall.enqueue(new okhttp3.Callback() { Call firstChunkCall = client.newCall(request);
compositeController.addController(new CallRequestController(firstChunkCall));
firstChunkCall.enqueue(new okhttp3.Callback() {
@Override @Override
public void onFailure(Call call, IOException e) { public void onFailure(@NonNull Call call, @NonNull IOException e) {
if (!compositeController.isCanceled()) { if (!compositeController.isCanceled()) {
callback.onFailure(e); callback.onFailure(e);
compositeController.cancel(); compositeController.cancel();
@ -65,9 +75,8 @@ public class ChunkedDataFetcher {
} }
@Override @Override
public void onResponse(Call call, Response response) throws IOException { public void onResponse(@NonNull Call call, @NonNull Response response) {
String contentLength = response.header("Content-Length"); String contentRange = response.header("Content-Range");
String acceptRanges = response.header("Accept-Ranges");
if (!response.isSuccessful()) { if (!response.isSuccessful()) {
Log.w(TAG, "Non-successful response code: " + response.code()); Log.w(TAG, "Non-successful response code: " + response.code());
@ -77,39 +86,64 @@ public class ChunkedDataFetcher {
return; return;
} }
if (TextUtils.isEmpty(contentLength)) { if (TextUtils.isEmpty(contentRange)) {
Log.w(TAG, "Missing Content-Length header."); Log.w(TAG, "Missing Content-Range header.");
callback.onFailure(new IOException("Missing Content-Length header.")); callback.onFailure(new IOException("Missing Content-Length header."));
compositeController.cancel(); compositeController.cancel();
if (response.body() != null) response.body().close(); if (response.body() != null) response.body().close();
return; return;
} }
long parsedContentLength; if (response.body() == null) {
try { Log.w(TAG, "Missing body.");
parsedContentLength = Long.parseLong(contentLength); callback.onFailure(new IOException("Missing body on initial request."));
} catch (NumberFormatException e) {
Log.w(TAG, "Invalid Content-Length value.");
callback.onFailure(new IOException("Invalid Content-Length value."));
compositeController.cancel(); compositeController.cancel();
return; return;
} }
if (response.body() != null) { Optional<Long> contentLength = parseLengthFromContentRange(contentRange);
response.body().close();
if (!contentLength.isPresent()) {
Log.w(TAG, "Unable to parse length from Content-Range.");
callback.onFailure(new IOException("Unable to get parse length from Content-Range."));
compositeController.cancel();
return;
} }
fetchChunks(url, parsedContentLength, compositeController, callback); if (chunkSize >= contentLength.get()) {
try {
callback.onSuccess(response.body().byteStream());
} catch (IOException e) {
callback.onFailure(e);
compositeController.cancel();
}
} else {
InputStream stream = ContentLengthInputStream.obtain(response.body().byteStream(), chunkSize);
fetchChunks(url, contentLength.get(), Optional.of(new Pair<>(stream, chunkSize)), compositeController, callback);
}
} }
}); });
return compositeController; return compositeController;
} }
private void fetchChunks(@NonNull String url, long contentLength, CompositeRequestController compositeController, Callback callback) { private void fetchChunks(@NonNull String url,
long contentLength,
Optional<Pair<InputStream, Long>> firstChunk,
CompositeRequestController compositeController,
Callback callback)
{
List<ByteRange> requestPattern; List<ByteRange> requestPattern;
try { try {
requestPattern = getRequestPattern(contentLength); if (firstChunk.isPresent()) {
requestPattern = Stream.of(getRequestPattern(contentLength - firstChunk.get().second()))
.map(b -> new ByteRange(b.start + firstChunk.get().second(),
b.end + firstChunk.get().second(),
b.ignoreFirst))
.toList();
} else {
requestPattern = getRequestPattern(contentLength);
}
} catch (IOException e) { } catch (IOException e) {
callback.onFailure(e); callback.onFailure(e);
compositeController.cancel(); compositeController.cancel();
@ -118,7 +152,11 @@ public class ChunkedDataFetcher {
SignalExecutors.IO.execute(() -> { SignalExecutors.IO.execute(() -> {
List<CallRequestController> controllers = Stream.of(requestPattern).map(range -> makeChunkRequest(client, url, range)).toList(); List<CallRequestController> controllers = Stream.of(requestPattern).map(range -> makeChunkRequest(client, url, range)).toList();
List<InputStream> streams = new ArrayList<>(controllers.size()); List<InputStream> streams = new ArrayList<>(controllers.size() + (firstChunk.isPresent() ? 1 : 0));
if (firstChunk.isPresent()) {
streams.add(firstChunk.get().first());
}
Stream.of(controllers).forEach(compositeController::addController); Stream.of(controllers).forEach(compositeController::addController);
@ -157,12 +195,12 @@ public class ChunkedDataFetcher {
call.enqueue(new okhttp3.Callback() { call.enqueue(new okhttp3.Callback() {
@Override @Override
public void onFailure(Call call, IOException e) { public void onFailure(@NonNull Call call, @NonNull IOException e) {
callController.cancel(); callController.cancel();
} }
@Override @Override
public void onResponse(Call call, Response response) throws IOException { public void onResponse(@NonNull Call call, @NonNull Response response) {
if (!response.isSuccessful()) { if (!response.isSuccessful()) {
callController.cancel(); callController.cancel();
if (response.body() != null) response.body().close(); if (response.body() != null) response.body().close();
@ -183,6 +221,22 @@ public class ChunkedDataFetcher {
return callController; return callController;
} }
private Optional<Long> parseLengthFromContentRange(@NonNull String contentRange) {
int totalStartPos = contentRange.indexOf('/');
if (totalStartPos >= 0 && contentRange.length() > totalStartPos + 1) {
String totalString = contentRange.substring(totalStartPos + 1);
try {
return Optional.of(Long.parseLong(totalString));
} catch (NumberFormatException e) {
return Optional.absent();
}
}
return Optional.absent();
}
private List<ByteRange> getRequestPattern(long size) throws IOException { private List<ByteRange> getRequestPattern(long size) throws IOException {
if (size > MB) return getRequestPattern(size, MB); if (size > MB) return getRequestPattern(size, MB);
else if (size > 500 * KB) return getRequestPattern(size, 500 * KB); else if (size > 500 * KB) return getRequestPattern(size, 500 * KB);

View File

@ -14,6 +14,7 @@ import java.net.InetSocketAddress;
import java.net.Proxy; import java.net.Proxy;
import java.net.ProxySelector; import java.net.ProxySelector;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.net.SocketException;
import java.net.URI; import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
@ -48,6 +49,10 @@ public class ContentProxySelector extends ProxySelector {
@Override @Override
public void connectFailed(URI uri, SocketAddress address, IOException failure) { public void connectFailed(URI uri, SocketAddress address, IOException failure) {
Log.w(TAG, "Connection failed.", failure); if (failure instanceof SocketException) {
Log.d(TAG, "Socket exception. Likely a cancellation.");
} else {
Log.w(TAG, "Connection failed.", failure);
}
} }
} }