mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-25 09:17:44 +00:00
Request a small chunk instead of HEAD for images of unknown size.
This commit is contained in:
parent
29cdb5290b
commit
f3f6cc87d9
@ -9,11 +9,13 @@ import com.bumptech.glide.util.ContentLengthInputStream;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@ -41,23 +43,31 @@ public class ChunkedDataFetcher {
|
||||
|
||||
public RequestController fetch(@NonNull String url, long contentLength, @NonNull Callback callback) {
|
||||
if (contentLength <= 0) {
|
||||
return fetch(url, callback);
|
||||
return fetchChunksWithUnknownTotalSize(url, callback);
|
||||
}
|
||||
|
||||
CompositeRequestController compositeController = new CompositeRequestController();
|
||||
fetchChunks(url, contentLength, compositeController, callback);
|
||||
fetchChunks(url, contentLength, Optional.absent(), compositeController, callback);
|
||||
return compositeController;
|
||||
}
|
||||
|
||||
public RequestController fetch(@NonNull String url, @NonNull Callback callback) {
|
||||
private RequestController fetchChunksWithUnknownTotalSize(@NonNull String url, @NonNull Callback callback) {
|
||||
CompositeRequestController compositeController = new CompositeRequestController();
|
||||
|
||||
Call headCall = client.newCall(new Request.Builder().url(url).head().cacheControl(NO_CACHE).build());
|
||||
compositeController.addController(new CallRequestController(headCall));
|
||||
long chunkSize = new SecureRandom().nextInt(1024) + 1024;
|
||||
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
|
||||
public void onFailure(Call call, IOException e) {
|
||||
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
||||
if (!compositeController.isCanceled()) {
|
||||
callback.onFailure(e);
|
||||
compositeController.cancel();
|
||||
@ -65,9 +75,8 @@ public class ChunkedDataFetcher {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
String contentLength = response.header("Content-Length");
|
||||
String acceptRanges = response.header("Accept-Ranges");
|
||||
public void onResponse(@NonNull Call call, @NonNull Response response) {
|
||||
String contentRange = response.header("Content-Range");
|
||||
|
||||
if (!response.isSuccessful()) {
|
||||
Log.w(TAG, "Non-successful response code: " + response.code());
|
||||
@ -77,39 +86,64 @@ public class ChunkedDataFetcher {
|
||||
return;
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(contentLength)) {
|
||||
Log.w(TAG, "Missing Content-Length header.");
|
||||
if (TextUtils.isEmpty(contentRange)) {
|
||||
Log.w(TAG, "Missing Content-Range header.");
|
||||
callback.onFailure(new IOException("Missing Content-Length header."));
|
||||
compositeController.cancel();
|
||||
if (response.body() != null) response.body().close();
|
||||
return;
|
||||
}
|
||||
|
||||
long parsedContentLength;
|
||||
try {
|
||||
parsedContentLength = Long.parseLong(contentLength);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w(TAG, "Invalid Content-Length value.");
|
||||
callback.onFailure(new IOException("Invalid Content-Length value."));
|
||||
if (response.body() == null) {
|
||||
Log.w(TAG, "Missing body.");
|
||||
callback.onFailure(new IOException("Missing body on initial request."));
|
||||
compositeController.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.body() != null) {
|
||||
response.body().close();
|
||||
Optional<Long> contentLength = parseLengthFromContentRange(contentRange);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
try {
|
||||
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) {
|
||||
callback.onFailure(e);
|
||||
compositeController.cancel();
|
||||
@ -118,7 +152,11 @@ public class ChunkedDataFetcher {
|
||||
|
||||
SignalExecutors.IO.execute(() -> {
|
||||
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);
|
||||
|
||||
@ -157,12 +195,12 @@ public class ChunkedDataFetcher {
|
||||
|
||||
call.enqueue(new okhttp3.Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
||||
callController.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
public void onResponse(@NonNull Call call, @NonNull Response response) {
|
||||
if (!response.isSuccessful()) {
|
||||
callController.cancel();
|
||||
if (response.body() != null) response.body().close();
|
||||
@ -183,6 +221,22 @@ public class ChunkedDataFetcher {
|
||||
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 {
|
||||
if (size > MB) return getRequestPattern(size, MB);
|
||||
else if (size > 500 * KB) return getRequestPattern(size, 500 * KB);
|
||||
|
@ -14,6 +14,7 @@ import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
@ -48,6 +49,10 @@ public class ContentProxySelector extends ProxySelector {
|
||||
|
||||
@Override
|
||||
public void connectFailed(URI uri, SocketAddress address, IOException failure) {
|
||||
if (failure instanceof SocketException) {
|
||||
Log.d(TAG, "Socket exception. Likely a cancellation.");
|
||||
} else {
|
||||
Log.w(TAG, "Connection failed.", failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user