Files
lineageos_updater/src/org/lineageos/updater/download/HttpURLConnectionClient.java
Gabriele M 0e79b791f0 Add support for duplicate links
Support duplicate links [1] to handle better temporarily unavailable
mirrors.

[1] https://tools.ietf.org/html/rfc6249

Change-Id: If78fb4a90da68ef221294eed2c59063a14cf1f43
2018-01-23 22:41:40 +01:00

312 lines
11 KiB
Java

/*
* 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.Comparator;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class HttpURLConnectionClient implements DownloadClient {
private final static String TAG = "HttpURLConnectionClient";
private HttpURLConnection mClient;
private final File mDestination;
private final DownloadClient.ProgressListener mProgressListener;
private final DownloadClient.DownloadCallback mCallback;
private final boolean mUseDuplicateLinks;
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,
boolean useDuplicateLinks) throws IOException {
mClient = (HttpURLConnection) new URL(url).openConnection();
mDestination = destination;
mProgressListener = progressListener;
mCallback = callback;
mUseDuplicateLinks = useDuplicateLinks;
}
@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 isRedirectCode(int statusCode) {
return (statusCode / 100) == 3;
}
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;
}
}
private void changeClientUrl(URL newUrl) throws IOException {
String range = mClient.getRequestProperty("Range");
mClient.disconnect();
mClient = (HttpURLConnection) newUrl.openConnection();
if (range != null) {
mClient.setRequestProperty("Range", range);
}
}
private void handleDuplicateLinks() throws IOException {
String protocol = mClient.getURL().getProtocol();
class DuplicateLink {
private String mUrl;
private int mPriority;
private DuplicateLink(String url, int priority) {
mUrl = url;
mPriority = priority;
}
}
PriorityQueue<DuplicateLink> duplicates = null;
for (Map.Entry<String, List<String>> entry : mClient.getHeaderFields().entrySet()) {
if ("Link".equalsIgnoreCase((entry.getKey()))) {
duplicates = new PriorityQueue<>(entry.getValue().size(),
new Comparator<DuplicateLink>() {
@Override
public int compare(DuplicateLink d1, DuplicateLink d2) {
return Integer.compare(d1.mPriority, d2.mPriority);
}
});
// https://tools.ietf.org/html/rfc6249
// https://tools.ietf.org/html/rfc5988#section-5
String regex = "(?i)<(.+)>\\s*;\\s*rel=duplicate(?:.*pri=([0-9]+).*|.*)?";
Pattern pattern = Pattern.compile(regex);
for (String field : entry.getValue()) {
Matcher matcher = pattern.matcher(field);
if (matcher.matches()) {
String url = matcher.group(1);
String pri = matcher.group(2);
int priority = pri != null ? Integer.parseInt(pri) : 999999;
duplicates.add(new DuplicateLink(url, priority));
Log.d(TAG, "Adding duplicate link " + url);
} else {
Log.d(TAG, "Ignoring link " + field);
}
}
}
}
String newUrl = mClient.getHeaderField("Location");
for (;;) {
try {
URL url = new URL(newUrl);
if (!url.getProtocol().equals(protocol)) {
// If we hadn't handled duplicate links, we wouldn't have
// used this url.
throw new IOException("Protocol changes are not allowed");
}
Log.d(TAG, "Downloading from " + newUrl);
changeClientUrl(url);
mClient.setConnectTimeout(5000);
mClient.connect();
if (!isSuccessCode(mClient.getResponseCode())) {
throw new IOException("Server replied with " + mClient.getResponseCode());
}
return;
} catch (IOException e) {
if (duplicates != null && !duplicates.isEmpty()) {
DuplicateLink link = duplicates.poll();
duplicates.remove(link);
newUrl = link.mUrl;
Log.e(TAG, "Using duplicate link " + link.mUrl, e);
} else {
throw e;
}
}
}
}
@Override
public void run() {
try {
mClient.setInstanceFollowRedirects(!mUseDuplicateLinks);
mClient.connect();
int responseCode = mClient.getResponseCode();
if (mUseDuplicateLinks && isRedirectCode(responseCode)) {
handleDuplicateLinks();
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();
}
}
}
}