diff --git a/Android.mk b/Android.mk index 6b61890e..835e737f 100644 --- a/Android.mk +++ b/Android.mk @@ -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 \ diff --git a/src/org/lineageos/updater/UpdatesActivity.java b/src/org/lineageos/updater/UpdatesActivity.java index 04aa5b37..1e0e3c46 100644 --- a/src/org/lineageos/updater/UpdatesActivity.java +++ b/src/org/lineageos/updater/UpdatesActivity.java @@ -385,11 +385,18 @@ public class UpdatesActivity extends UpdatesListActivity { } }; - final DownloadClient downloadClient = new DownloadClient.Builder() - .setUrl(url) - .setDestination(jsonFileTmp) - .setDownloadCallback(callback) - .build(); + 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 diff --git a/src/org/lineageos/updater/UpdatesCheckReceiver.java b/src/org/lineageos/updater/UpdatesCheckReceiver.java index fcc5132c..b70d388f 100644 --- a/src/org/lineageos/updater/UpdatesCheckReceiver.java +++ b/src/org/lineageos/updater/UpdatesCheckReceiver.java @@ -105,12 +105,17 @@ public class UpdatesCheckReceiver extends BroadcastReceiver { } }; - DownloadClient downloadClient = new DownloadClient.Builder() - .setUrl(url) - .setDestination(jsonNew) - .setDownloadCallback(callback) - .build(); - downloadClient.start(); + 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) { diff --git a/src/org/lineageos/updater/controller/UpdaterController.java b/src/org/lineageos/updater/controller/UpdaterController.java index 8364b15d..6ac9e26f 100644 --- a/src/org/lineageos/updater/controller/UpdaterController.java +++ b/src/org/lineageos/updater/controller/UpdaterController.java @@ -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() - .setUrl(update.getDownloadUrl()) - .setDestination(update.getFile()) - .setDownloadCallback(getDownloadCallback(downloadId)) - .setProgressListener(getProgressListener(downloadId)) - .build(); + 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() - .setUrl(update.getDownloadUrl()) - .setDestination(update.getFile()) - .setDownloadCallback(getDownloadCallback(downloadId)) - .setProgressListener(getProgressListener(downloadId)) - .build(); + 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); diff --git a/src/org/lineageos/updater/download/DownloadClient.java b/src/org/lineageos/updater/download/DownloadClient.java index 817c59d6..8ccf3e8a 100644 --- a/src/org/lineageos/updater/download/DownloadClient.java +++ b/src/org/lineageos/updater/download/DownloadClient.java @@ -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) { diff --git a/src/org/lineageos/updater/download/HttpURLConnectionClient.java b/src/org/lineageos/updater/download/HttpURLConnectionClient.java new file mode 100644 index 00000000..89d47afe --- /dev/null +++ b/src/org/lineageos/updater/download/HttpURLConnectionClient.java @@ -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> 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(); + } + } + } +} diff --git a/src/org/lineageos/updater/download/OkHttpDownloadClient.java b/src/org/lineageos/updater/download/OkHttpDownloadClient.java deleted file mode 100644 index 06a0812c..00000000 --- a/src/org/lineageos/updater/download/OkHttpDownloadClient.java +++ /dev/null @@ -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> 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; - } - }; - } - } -}