package com.matsuhiro.android.download;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import org.apache.http.conn.ConnectTimeoutException;
import android.accounts.NetworkErrorException;
import android.content.Context;
import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.Log;
import com.matsuhiro.android.connect.NetworkUtils;
import com.matsuhiro.android.storage.StorageUtils;
public class DownloadTask extends AsyncTask<Void, Integer, Long> {
public final static int TIME_OUT = 30000;
private final static int BUFFER_SIZE = 1024 * 8;
private static final String TAG = DownloadTask.class.getSimpleName();
private static final boolean DEBUG = true;
public static final String TEMP_SUFFIX = ".download";
private HttpURLConnection mConnection = null;
private File mFile;
private File mTempFile;
private String mUrlString;
private URL mURL;
private DownloadTaskListener mListener;
private Context mContext;
private long mDownloadSize;
private long mPreviousFileSize;
private long mTotalSize;
private int mDownloadPercent;
private long mNetworkSpeed;
private long mPreviousTime;
private long mTotalTime;
private Throwable mError = null;
//private Throwable mIpError = null;
private boolean mInterrupt = false;
private long downloadId = -1;
private boolean mCheckLink;
private final class ProgressReportingRandomAccessFile extends RandomAccessFile {
private int progress = 0;
public ProgressReportingRandomAccessFile(File file, String mode)
throws FileNotFoundException {
super(file, mode);
}
@Override
public void write(byte[] buffer, int offset, int count) throws IOException {
super.write(buffer, offset, count);
progress += count;
publishProgress(progress);
}
}
/*public DownloadTask(Context context, long id, String url, String name, String path, boolean checkIp)
throws MalformedURLException {
this(context, id, url, name, path, null, checkIp);
}
public DownloadTask(Context context, long id, String url, String path, DownloadTaskListener listener, boolean checkIp)
throws MalformedURLException {
this(context, id, url, null, path, listener, checkIp);
}*/
public DownloadTask(Context context, long id, String url, String name, String path, DownloadTaskListener listener, boolean checkLink)
throws MalformedURLException {
super();
this.mUrlString = url;
this.mListener = listener;
this.mURL = new URL(url);
this.mCheckLink = checkLink;
if (TextUtils.isEmpty(name))
name = new File(mURL.getFile()).getName();
this.mFile = new File(path, name);
this.mTempFile = new File(path, name + TEMP_SUFFIX);
this.mContext = context;
this.downloadId = id;
}
/*public String getUrl() {
return mUrlString;
}
public boolean isInterrupt() {
return mInterrupt;
}
public int getDownloadPercent() {
return mDownloadPercent;
}*/
public long getDownloadId() {
return this.downloadId;
}
/*public Map<Long, Integer> getDownloadPercentMap() {
return Maps.mDownloadPercentMap;
}*/
public long getDownloadSize() {
return mDownloadSize + mPreviousFileSize;
}
/*public Map<Long, Long> getDownloadSizeMap() {
return Maps.mDownloadSizeMap;
}
public long getTotalSize() {
return mTotalSize;
}
public Map<Long, Long> getTotalSizeMap() {
return Maps.mTotalSizeMap;
}
public long getDownloadSpeed() {
return this.mNetworkSpeed;
}
public long getTotalTime() {
return this.mTotalTime;
}
public DownloadTaskListener getListener() {
return this.mListener;
}*/
public String getDownloadedFileName() {
return this.mFile.getName();
}
@Override
protected void onPreExecute() {
mPreviousTime = System.currentTimeMillis();
if (mListener != null)
mListener.preDownload(this);
}
@Override
protected Long doInBackground(Void... params) {
//mThreadId = Thread.currentThread().getId();
//Log.v(TAG, "doInBackground thread ID: " + mThreadId);
long result = -1;
try {
result = download();
} catch (NetworkErrorException e) {
mError = e;
} catch (FileAlreadyExistException e) {
mError = e;
} catch (NoMemoryException e) {
mError = e;
} catch (IOException e) {
mError = e;
} catch (SpecifiedUrlIsNotFoundException e) {
mError = e;
} catch (OtherHttpErrorException e) {
mError = e;
} catch (InvalidYoutubeLinkException e) {
//mIpError = e;
mError = e;
} finally {
if (mConnection != null) {
mConnection.disconnect();
mConnection = null;
}
}
return result;
}
@Override
protected void onProgressUpdate(Integer... progress) {
if (progress.length > 1) {
mTotalSize = progress[1];
Maps.mTotalSizeMap.put(downloadId, mTotalSize);
if (mTotalSize == -1) {
if (mListener != null) {
mListener.errorDownload(this, mError);
return;
}
}
}
mTotalTime = System.currentTimeMillis() - mPreviousTime;
mDownloadSize = progress[0];
Maps.mDownloadSizeMap.put(downloadId, mDownloadSize + mPreviousFileSize);
if (mTotalSize == 0) {
mDownloadPercent = -1;
} else {
mDownloadPercent = (int) ((mDownloadSize + mPreviousFileSize) * 100 / mTotalSize);
}
Maps.mDownloadPercentMap.put(downloadId, mDownloadPercent);
mNetworkSpeed = mDownloadSize / mTotalTime;
Maps.mNetworkSpeedMap.put(downloadId, mNetworkSpeed);
if (mListener != null)
mListener.updateProcess(this);
}
@Override
protected void onPostExecute(Long result) {
if (result == -1 || mInterrupt || mError != null/* || mIpError != null*/) {
if (DEBUG && mError != null) {
Log.w(TAG, "Download failed. " + mError.getMessage());
/*if (mListener != null) {
mListener.errorDownload(this, mError);
}*/
}
if (mListener != null) {
mListener.errorDownload(this, mError);
}
/*if (DEBUG && mIpError != null) {
Log.w(TAG, "Download resumed. " + mIpError.getMessage());
if (mListener != null) {
mListener.resumeFromDifferentIp(this, mIpError);
}
}*/
return;
}
// finish download
mTempFile.renameTo(mFile);
if (mListener != null)
mListener.finishDownload(this);
}
@Override
public void onCancelled() {
super.onCancelled();
mInterrupt = true;
}
private long download() throws NetworkErrorException, IOException,
SpecifiedUrlIsNotFoundException, FileAlreadyExistException,
NoMemoryException, OtherHttpErrorException, InvalidYoutubeLinkException {
if (DEBUG) {
Log.v(TAG, "totalSize: " + mTotalSize);
}
/*
* check network
*/
if (!NetworkUtils.isNetworkAvailable(mContext)) {
throw new NetworkErrorException("Network blocked.");
}
/*
* check expire time
*/
if (mCheckLink) {
long exp = NetworkUtils.findLinkExpireTime(mUrlString);
Log.i(TAG, "link expires at: " + exp);
long ct = System.currentTimeMillis() / 1000;
Log.i(TAG, "current time is: " + ct);
if (ct > exp - 120 /* 2 min as buffer */) {
throw new InvalidYoutubeLinkException("Youtube link expired.");
}
}
/*
* check ip
*/
if (mCheckLink) {
String linkIp = NetworkUtils.findLinkIp(mUrlString);
Log.i(TAG, "initial request IP: " + linkIp);
String actualExtIp = NetworkUtils.getExternalIpAddress();
Log.i(TAG, "current request IP: " + actualExtIp);
if (!linkIp.equals(actualExtIp) || actualExtIp == null) {
throw new InvalidYoutubeLinkException("IP is different from initial request.");
}
}
/*
* check file length
*/
String userAgent = NetworkUtils.getUserAgent(mContext);
mConnection = (HttpURLConnection) mURL.openConnection();
mConnection.setRequestMethod("GET");
mConnection.setRequestProperty("User-Agent", userAgent);
mConnection.setRequestProperty("Accept-Encoding", "identity");
if (mTempFile.exists()) {
mPreviousFileSize = mTempFile.length();
mConnection.setRequestProperty("Range", "bytes=" + mPreviousFileSize + "-");
}
mConnection.connect();
int responseCode = mConnection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
throw new SpecifiedUrlIsNotFoundException("Not found: " + mUrlString);
} else if (responseCode != HttpURLConnection.HTTP_OK
&& responseCode != HttpURLConnection.HTTP_PARTIAL) {
String responseCodeString = Integer.toString(responseCode);
throw new OtherHttpErrorException("http error code: " + responseCodeString, responseCodeString);
}
boolean isRangeDownload = false;
int length = mConnection.getContentLength();
if (responseCode == HttpURLConnection.HTTP_PARTIAL) {
length += mPreviousFileSize;
isRangeDownload = true;
}
if (mFile.exists() && length == mFile.length()) {
if (DEBUG) {
Log.w(TAG, "Output file already exists. Skipping download.");
}
throw new FileAlreadyExistException("Output file already exists. Skipping download.");
}
/*
* check memory
*/
long storage = StorageUtils.getAvailableStorage();
if (DEBUG) {
Log.v(TAG, "storage:" + storage + " totalSize:" + length);
}
if (length - mPreviousFileSize > storage) {
throw new NoMemoryException("SD card no memory.");
}
RandomAccessFile outputStream = new ProgressReportingRandomAccessFile(mTempFile, "rw");
InputStream inputStream = mConnection.getInputStream();
publishProgress(0, length);
int bytesCopied = copy(inputStream, outputStream, isRangeDownload);
if ((mPreviousFileSize + bytesCopied) != mTotalSize && mTotalSize != -1 && !mInterrupt) {
throw new IOException("Download incomplete: " + bytesCopied + " != " + mTotalSize);
}
if (DEBUG) {
Log.d(TAG, "Download completed successfully.");
}
return bytesCopied;
}
private int copy(InputStream input, RandomAccessFile output, boolean isRangeDownload)
throws IOException, NetworkErrorException {
if (input == null || output == null) {
return -1;
}
byte[] buffer = new byte[BUFFER_SIZE];
BufferedInputStream in = new BufferedInputStream(input, BUFFER_SIZE);
if (DEBUG) {
Log.v(TAG, "length " + output.length());
}
int count = 0, n = 0;
long errorBlockTimePreviousTime = -1, expireTime = 0;
try {
if (isRangeDownload) {
output.seek(output.length());
}
while (!mInterrupt) {
n = in.read(buffer, 0, BUFFER_SIZE);
if (n == -1) {
break;
}
output.write(buffer, 0, n);
count += n;
/*
* check network
*/
if (!NetworkUtils.isNetworkAvailable(mContext)) {
throw new NetworkErrorException("Network blocked.");
}
if (mNetworkSpeed == 0) {
if (errorBlockTimePreviousTime > 0) {
expireTime = System.currentTimeMillis() - errorBlockTimePreviousTime;
if (expireTime > TIME_OUT) {
throw new ConnectTimeoutException("connection time out.");
}
} else {
errorBlockTimePreviousTime = System.currentTimeMillis();
}
} else {
expireTime = 0;
errorBlockTimePreviousTime = -1;
}
}
} finally {
mConnection.disconnect();
mConnection = null;
output.close();
in.close();
input.close();
}
return count;
}
public void cancel() {
this.cancel(true);
mInterrupt = true;
Log.d(TAG, "cancel on id " + downloadId);
}
}