Implement DownloadClient using HttpURLConnection

The version of OkHttp used in AOSP doesn't handle dynamic table size
updates [1] properly [2]. Instead of fixing OkHttp or importing a
prebuilt updated version, implement a new download client only using
HttpURLConnection, which seems to work properly.

[1] https://tools.ietf.org/html/rfc7541#section-6.3
[2] https://trac.nginx.org/nginx/ticket/1397

Change-Id: I3eedf7326f2017812c4a12d41f9ea028d255f7a8
This commit is contained in:
Gabriele M
2017-11-11 15:44:29 +01:00
parent 7aa3a999d8
commit 9dc1349c1a
7 changed files with 267 additions and 324 deletions

View File

@@ -12,8 +12,7 @@ LOCAL_STATIC_JAVA_LIBRARIES := \
android-support-v7-appcompat \
android-support-v7-cardview \
android-support-v7-preference \
android-support-v7-recyclerview \
okhttp
android-support-v7-recyclerview
LOCAL_RESOURCE_DIR := \
$(TOP)/frameworks/support/design/res \

View File

@@ -385,11 +385,18 @@ public class UpdatesActivity extends UpdatesListActivity {
}
};
final DownloadClient downloadClient = new DownloadClient.Builder()
final DownloadClient downloadClient;
try {
downloadClient = new DownloadClient.Builder()
.setUrl(url)
.setDestination(jsonFileTmp)
.setDownloadCallback(callback)
.build();
} catch (IOException exception) {
Log.e(TAG, "Could not build download client");
showSnackbar(R.string.snack_updates_check_failed, Snackbar.LENGTH_LONG);
return;
}
progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override

View File

@@ -105,12 +105,17 @@ public class UpdatesCheckReceiver extends BroadcastReceiver {
}
};
try {
DownloadClient downloadClient = new DownloadClient.Builder()
.setUrl(url)
.setDestination(jsonNew)
.setDownloadCallback(callback)
.build();
downloadClient.start();
} catch (IOException e) {
Log.e(TAG, "Could not fetch list, scheduling new check", e);
scheduleUpdatesCheck(context);
}
}
private static void showNotification(Context context) {

View File

@@ -31,6 +31,7 @@ import org.lineageos.updater.model.UpdateInfo;
import org.lineageos.updater.model.UpdateStatus;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
@@ -365,12 +366,20 @@ public class UpdaterController implements Controller {
Log.d(TAG, "Changing name with " + destination.getName());
}
update.setFile(destination);
DownloadClient downloadClient = new DownloadClient.Builder()
DownloadClient downloadClient;
try {
downloadClient = new DownloadClient.Builder()
.setUrl(update.getDownloadUrl())
.setDestination(update.getFile())
.setDownloadCallback(getDownloadCallback(downloadId))
.setProgressListener(getProgressListener(downloadId))
.build();
} catch (IOException exception) {
Log.e(TAG, "Could not build download client");
update.setStatus(UpdateStatus.PAUSED_ERROR);
notifyUpdateChange(downloadId);
return false;
}
addDownloadClient(mDownloads.get(downloadId), downloadClient);
update.setStatus(UpdateStatus.STARTING);
notifyUpdateChange(downloadId);
@@ -399,12 +408,20 @@ public class UpdaterController implements Controller {
verifyUpdateAsync(downloadId);
notifyUpdateChange(downloadId);
} else {
DownloadClient downloadClient = new DownloadClient.Builder()
DownloadClient downloadClient;
try {
downloadClient = new DownloadClient.Builder()
.setUrl(update.getDownloadUrl())
.setDestination(update.getFile())
.setDownloadCallback(getDownloadCallback(downloadId))
.setProgressListener(getProgressListener(downloadId))
.build();
} catch (IOException exception) {
Log.e(TAG, "Could not build download client");
update.setStatus(UpdateStatus.PAUSED_ERROR);
notifyUpdateChange(downloadId);
return false;
}
addDownloadClient(mDownloads.get(downloadId), downloadClient);
update.setStatus(UpdateStatus.STARTING);
notifyUpdateChange(downloadId);

View File

@@ -16,6 +16,7 @@
package org.lineageos.updater.download;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@@ -63,7 +64,7 @@ public interface DownloadClient {
private DownloadClient.DownloadCallback mCallback;
private DownloadClient.ProgressListener mProgressListener;
public DownloadClient build() {
public DownloadClient build() throws IOException {
if (mUrl == null) {
throw new IllegalStateException("No download URL defined");
} else if (mDestination == null) {
@@ -71,7 +72,7 @@ public interface DownloadClient {
} else if (mCallback == null) {
throw new IllegalStateException("No download callback defined");
}
return new OkHttpDownloadClient(mUrl, mDestination, mProgressListener, mCallback);
return new HttpURLConnectionClient(mUrl, mDestination, mProgressListener, mCallback);
}
public Builder setUrl(String url) {

View File

@@ -0,0 +1,211 @@
/*
* Copyright (C) 2017 The LineageOS Project
*
* Licensed 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.lineageos.updater.download;
import android.os.SystemClock;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Map;
public class HttpURLConnectionClient implements DownloadClient {
private final static String TAG = "HttpURLConnectionClient";
private final HttpURLConnection mClient;
private final File mDestination;
private final DownloadClient.ProgressListener mProgressListener;
private final DownloadClient.DownloadCallback mCallback;
private DownloadThread mDownloadThread;
public class Headers implements DownloadClient.Headers {
@Override
public String get(String name) {
return mClient.getHeaderField(name);
}
@Override
public Map<String, List<String>> getAll() {
return mClient.getHeaderFields();
}
}
HttpURLConnectionClient(String url, File destination,
DownloadClient.ProgressListener progressListener,
DownloadClient.DownloadCallback callback) throws IOException {
mClient = (HttpURLConnection) new URL(url).openConnection();
mDestination = destination;
mProgressListener = progressListener;
mCallback = callback;
}
@Override
public void start() {
if (mDownloadThread != null) {
Log.e(TAG, "Already downloading");
return;
}
downloadFileInternalCommon(false);
}
@Override
public void resume() {
if (mDownloadThread != null) {
Log.e(TAG, "Already downloading");
return;
}
downloadFileResumeInternal();
}
@Override
public void cancel() {
if (mDownloadThread == null) {
Log.e(TAG, "Not downloading");
return;
}
mDownloadThread.interrupt();
mDownloadThread = null;
}
private void downloadFileResumeInternal() {
if (!mDestination.exists()) {
mCallback.onFailure(false);
return;
}
long offset = mDestination.length();
mClient.setRequestProperty("Range", "bytes=" + offset + "-");
downloadFileInternalCommon(true);
}
private void downloadFileInternalCommon(boolean resume) {
if (mDownloadThread != null) {
Log.wtf(TAG, "Already downloading");
return;
}
mDownloadThread = new DownloadThread(resume);
mDownloadThread.start();
}
private static boolean isSuccessCode(int statusCode) {
return (statusCode / 100) == 2;
}
private static boolean isPartialContentCode(int statusCode) {
return statusCode == 206;
}
private class DownloadThread extends Thread {
private long mTotalBytes = 0;
private long mTotalBytesRead = 0;
private long mCurSampleBytes = 0;
private long mLastMillis = 0;
private long mSpeed = -1;
private long mEta = -1;
private final boolean mResume;
private DownloadThread(boolean resume) {
mResume = resume;
}
private void calculateSpeed() {
final long millis = SystemClock.elapsedRealtime();
final long delta = millis - mLastMillis;
if (delta > 500) {
final long curSpeed = ((mTotalBytesRead - mCurSampleBytes) * 1000) / delta;
if (mSpeed == -1) {
mSpeed = curSpeed;
} else {
mSpeed = ((mSpeed * 3) + curSpeed) / 4;
}
mLastMillis = millis;
mCurSampleBytes = mTotalBytesRead;
}
}
private void calculateEta() {
if (mSpeed > 0) {
mEta = (mTotalBytes - mTotalBytesRead) / mSpeed;
}
}
@Override
public void run() {
try {
mClient.connect();
int responseCode = mClient.getResponseCode();
mCallback.onResponse(responseCode, mClient.getURL().toString(), new Headers());
if (mResume && isPartialContentCode(responseCode)) {
mTotalBytesRead = mDestination.length();
Log.d(TAG, "The server fulfilled the partial content request");
} else if (mResume || !isSuccessCode(responseCode)) {
Log.e(TAG, "The server replied with code " + responseCode);
mCallback.onFailure(isInterrupted());
return;
}
try (
InputStream inputStream = mClient.getInputStream();
OutputStream outputStream = new FileOutputStream(mDestination, mResume)
) {
mTotalBytes = mClient.getContentLength() + mTotalBytesRead;
byte[] b = new byte[8192];
int count;
while (!isInterrupted() && (count = inputStream.read(b)) > 0) {
outputStream.write(b, 0, count);
mTotalBytesRead += count;
calculateSpeed();
calculateEta();
if (mProgressListener != null) {
mProgressListener.update(mTotalBytesRead, mTotalBytes, mSpeed, mEta,
false);
}
}
if (mProgressListener != null) {
mProgressListener.update(mTotalBytesRead, mTotalBytes, mSpeed, mEta, true);
}
outputStream.flush();
if (isInterrupted()) {
mCallback.onFailure(true);
} else {
mCallback.onSuccess(mDestination);
}
}
} catch (IOException e) {
Log.e(TAG, "Error downloading file", e);
mCallback.onFailure(isInterrupted());
} finally {
mClient.disconnect();
}
}
}
}

View File

@@ -1,297 +0,0 @@
/*
* Copyright (C) 2017 The LineageOS Project
*
* Licensed 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.lineageos.updater.download;
import android.os.SystemClock;
import android.util.Log;
import com.android.okhttp.Callback;
import com.android.okhttp.Interceptor;
import com.android.okhttp.MediaType;
import com.android.okhttp.OkHttpClient;
import com.android.okhttp.Request;
import com.android.okhttp.Response;
import com.android.okhttp.ResponseBody;
import com.android.okhttp.okio.Buffer;
import com.android.okhttp.okio.BufferedSink;
import com.android.okhttp.okio.BufferedSource;
import com.android.okhttp.okio.ForwardingSource;
import com.android.okhttp.okio.Okio;
import com.android.okhttp.okio.Source;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
class OkHttpDownloadClient implements DownloadClient {
private static final String TAG = "DownloadClient";
private final Object DOWNLOAD_TAG = new Object();
private final OkHttpClient mClient = new OkHttpClient();
private final String mUrl;
private final File mDestination;
private final DownloadClient.ProgressListener mProgressListener;
private final DownloadClient.DownloadCallback mCallback;
private long mResumeOffset = 0;
private boolean mDownloading = false;
private boolean mCancelled = false;
public class Headers implements DownloadClient.Headers {
private com.android.okhttp.Headers mHeaders;
private Headers(com.android.okhttp.Headers headers) {
mHeaders = headers;
}
@Override
public String get(String name) {
return mHeaders.get(name);
}
@Override
public Map<String, List<String>> getAll() {
return mHeaders.toMultimap();
}
}
OkHttpDownloadClient(String url, File destination,
DownloadClient.ProgressListener progressListener,
DownloadClient.DownloadCallback callback) {
mUrl = url;
mDestination = destination;
mProgressListener = progressListener;
mCallback = callback;
}
@Override
public void start() {
if (mDownloading) {
Log.e(TAG, "Already downloading");
return;
}
mCancelled = false;
mDownloading = true;
downloadFileInternal();
}
@Override
public void resume() {
if (mDownloading) {
Log.e(TAG, "Already downloading");
return;
}
mCancelled = false;
mDownloading = true;
downloadFileResumeInternal();
}
@Override
public void cancel() {
if (!mDownloading) {
Log.e(TAG, "Not downloading");
return;
}
mDownloading = false;
new Thread(new Runnable() {
@Override
public void run() {
mCancelled = true;
mClient.cancel(DOWNLOAD_TAG);
}
}).start();
}
private void downloadFileInternal() {
final Request request = new Request.Builder()
.url(mUrl)
.tag(DOWNLOAD_TAG)
.build();
downloadFileInternalCommon(request, false);
}
private void downloadFileResumeInternal() {
final Request.Builder requestBuilder = new Request.Builder()
.url(mUrl)
.tag(DOWNLOAD_TAG);
if (!mDestination.exists()) {
mCallback.onFailure(mCancelled);
return;
}
long offset = mDestination.length();
requestBuilder.addHeader("Range", "bytes=" + offset + "-");
final Request request = requestBuilder.build();
downloadFileInternalCommon(request, true);
}
private static boolean isSuccessCode(int statusCode) {
return (statusCode / 100) == 2;
}
private static boolean isPartialContentCode(int statusCode) {
return statusCode == 206;
}
private void downloadFileInternalCommon(final Request request, final boolean resume) {
mClient.networkInterceptors().add(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
ProgressResponseBody progressResponseBody =
new ProgressResponseBody(originalResponse.body(), mProgressListener);
return originalResponse.newBuilder()
.body(progressResponseBody)
.build();
}
});
mClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Request request, IOException e) {
Log.d(TAG, "Download failed", e);
mCallback.onFailure(mCancelled);
}
@Override
public void onResponse(Response response) {
Log.d(TAG, "Downloading");
final ResponseBody body = response.body();
if (resume && isPartialContentCode(response.code())) {
mResumeOffset = mDestination.length();
Log.d(TAG, "The server fulfilled the partial content request");
} else if (resume || !isSuccessCode(response.code())) {
Log.e(TAG, "The server replied with code " + response.code());
mCallback.onFailure(mCancelled);
try {
body.close();
} catch (IOException e) {
Log.e(TAG, "Could not close response body", e);
}
return;
}
mCallback.onResponse(response.code(), response.request().urlString(),
new Headers(response.headers()));
try (BufferedSink sink = Okio.buffer(resume ?
Okio.appendingSink(mDestination) : Okio.sink(mDestination))) {
sink.writeAll(body.source());
Log.d(TAG, "Download complete");
sink.flush();
mCallback.onSuccess(mDestination);
} catch (IOException e) {
onFailure(request, e);
} finally {
try {
body.close();
} catch (IOException e) {
Log.e(TAG, "Could not close response body", e);
}
}
}
});
}
private class ProgressResponseBody extends ResponseBody {
private final ResponseBody mResponseBody;
private final DownloadClient.ProgressListener mProgressListener;
private BufferedSource mBufferedSource;
ProgressResponseBody(ResponseBody responseBody,
DownloadClient.ProgressListener progressListener) {
mResponseBody = responseBody;
mProgressListener = progressListener;
}
@Override
public MediaType contentType() {
return mResponseBody.contentType();
}
@Override
public long contentLength() throws IOException {
return mResponseBody.contentLength();
}
@Override
public BufferedSource source() throws IOException {
if (mBufferedSource == null) {
mBufferedSource = Okio.buffer(source(mResponseBody.source()));
}
return mBufferedSource;
}
private Source source(Source source) {
return new ForwardingSource(source) {
private long mTotalBytes = 0;
private long mTotalBytesRead = mResumeOffset;
private long mCurSampleBytes = 0;
private long mLastMillis = 0;
private long mSpeed = -1;
private long mEta = -1;
private void calculateSpeed() {
final long millis = SystemClock.elapsedRealtime();
final long delta = millis - mLastMillis;
if (delta > 500) {
final long curSpeed = ((mTotalBytesRead - mCurSampleBytes) * 1000) / delta;
if (mSpeed == -1) {
mSpeed = curSpeed;
} else {
mSpeed = ((mSpeed * 3) + curSpeed) / 4;
}
mLastMillis = millis;
mCurSampleBytes = mTotalBytesRead;
}
}
private void calculateEta() {
if (mSpeed > 0) {
mEta = (mTotalBytes - mTotalBytesRead) / mSpeed;
}
}
@Override
public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
mTotalBytes = mResponseBody.contentLength() + mResumeOffset;
// read() returns the number of bytes read, or -1 if this source is exhausted.
mTotalBytesRead += bytesRead != -1 ? bytesRead : 0;
calculateSpeed();
calculateEta();
if (mProgressListener != null) {
mProgressListener.update(mTotalBytesRead, mTotalBytes,
mSpeed, mEta, bytesRead == -1);
}
return bytesRead;
}
};
}
}
}