package cn.koolcloud.ipos.appstore.download.providers;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicHeader;
import org.json.JSONObject;
import android.content.Context;
import android.os.Environment;
import android.os.StrictMode;
import android.util.Log;
import cn.koolcloud.ipos.appstore.AppStorePreference;
import cn.koolcloud.ipos.appstore.api.ApiService;
import cn.koolcloud.ipos.appstore.common.AsyncHttpClient;
import cn.koolcloud.ipos.appstore.download.common.DownloadConstants;
import cn.koolcloud.ipos.appstore.download.common.DownloadUtil;
import cn.koolcloud.ipos.appstore.download.common.DownloadVariable;
import cn.koolcloud.ipos.appstore.download.common.DownloaderErrorException;
import cn.koolcloud.ipos.appstore.download.database.DownloadDBOperator;
import cn.koolcloud.ipos.appstore.download.entity.DownloadBean;
import cn.koolcloud.ipos.appstore.utils.Logger;
public class Downloader {
private final static String TAG = "Downloader";
private final static byte[] lock_getFileSize = new byte[1];
private final static byte[] lock_refresh_progress = new byte[1];
private int mThreadCount = 4; //default sub thread num = 4
private int bufferSize = 1024 * 16; //one block is 16K
private DownloadBean mBean; // this is downloader's bean not sub bean
private Context mContext;
private DownloadEngineCallback mCallback;
private DownloadDBOperator mDBOper;
private int mDoneThreadCount = 0; // finished thread num
private int mState = DownloadConstants.DOWNLOAD_STATE_INIT; // downloader status
private ArrayList<DownloadBean> mBeans;
public Downloader(DownloadBean bean, Context context,
DownloadEngineCallback callback) {
this.mBean = bean;
this.mContext = context;
this.mCallback = callback;
this.mDBOper = DownloadDBOperator.getInstance(context);
this.mBeans = new ArrayList<DownloadBean>(mThreadCount);
}
public void setDownloadBean(DownloadBean bean) {
this.mBean = bean;
}
public void initBeanInDataBase() {
try {
if (this.mDBOper != null) {
//if this task is exist in database
if (this.mDBOper.isHasDownloadTaskByUrl(mBean.url,
mBean.downloadId)) {
getDownloaderInfoFromDB(mBean);
} else { // insert information to database
addDownloaderInfoToDB(mBean);
}
} else {
callBackError("Downloader error, DBOperator may be null.");
throw new DownloaderErrorException(
"Downloader error, DBOperator may be null.");
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void setDownloadEngineCallback(DownloadEngineCallback callBack) {
this.mCallback = callBack;
}
public DownloadBean getDownloaderInfo() {
return mBean;
}
public int getDownloaderState() {
return mState;
}
/**
* request initialization
* @param state
*/
protected void setDownloaderState(int state) {
mState = state;
if (state == DownloadConstants.DOWNLOAD_STATE_INIT) {
mBean.currentPosition = 0;
}
}
/**
* Add download information to database,
* used for just initialing downloader and this task not exist in database
*
* @param bean
* @throws DownloaderErrorException
*/
private void addDownloaderInfoToDB(DownloadBean bean)
throws DownloaderErrorException {
/*if (mState != DownloadConstants.DOWNLOAD_STATE_INIT
&& mState != DownloadConstants.DOWNLOAD_STATE_STOP
&& mState != DownloadConstants.DOWNLOAD_STATE_ERROR) {
callBackError("This task is already in database");
throw new DownloaderErrorException("This task is already in database");
}*/
if (mDBOper != null) {
long fileSize = bean.fileSize;
if (mBeans.size() > 0) {
mBeans.clear();
}
try {
//check file size. no need to access the network and init sub threads directly when size > 0
if (fileSize > 0) {
if (!hasSpaceInSDCard()) {
return;
}
//split file to sub blocks
long range = fileSize / mThreadCount;
for (int i = 0; i < mThreadCount - 1; i++) {
DownloadBean subBean = (DownloadBean) bean.clone();
subBean.threadId = i;
subBean.startPosition = i * range;
subBean.endPosition = (i + 1) * range - 1;
mBeans.add(subBean);
}
DownloadBean subBean = (DownloadBean) bean.clone();
subBean.threadId = mThreadCount - 1;
subBean.startPosition = (mThreadCount - 1) * range;
subBean.endPosition = fileSize - 1;
mBeans.add(subBean);
} else {// init N sub downloaders directly with size 0, when file size = 0
for (int n = 0; n < mThreadCount - 1; n++) {
DownloadBean subBean = (DownloadBean) bean.clone();
subBean.threadId = n;
mBeans.add(subBean);
}
DownloadBean subBean = (DownloadBean) bean.clone();
subBean.threadId = mThreadCount - 1;
mBeans.add(subBean);
}
mDBOper.addDownloadTask(mBeans);
//set to waiting status when the file size already got.
if (bean.fileSize > 0) {
mState = DownloadConstants.DOWNLOAD_STATE_WAITTING; // downloader is waiting
} else {// to get file size from network and update sub downloaders when file size not got yet
new Thread(new Runnable() {
@Override
public void run() {
boolean flag = false;
synchronized (lock_getFileSize) {
flag = getFileSizeByNetwork(mBean);
}
if (flag) {
mState = DownloadConstants.DOWNLOAD_STATE_WAITTING;
} else {
Log.e(TAG, "get file size error from network 1");
}
}
}).start();
}
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
} else {
callBackError("add Downloader Info To DB error, DBOperator maybe null");
throw new DownloaderErrorException(
"add Downloader Info To DB error, DBOperator maybe null");
}
}
/**
* get downloader information from database
*
* @param bean
* @throws DownloaderErrorException
*/
private void getDownloaderInfoFromDB(DownloadBean bean)
throws DownloaderErrorException {
if (mDBOper != null) {
mBeans.clear();
mBeans = mDBOper.getDownloadTaskByUrl(bean.url, bean.downloadId);
mBean.currentPosition = 0;
mBean.fileSize = 0;
mThreadCount = mBeans.size();
for (DownloadBean subBean : mBeans) {
mBean.currentPosition += subBean.currentPosition;
if (subBean.fileSize > mBean.fileSize) {
mBean.fileSize = subBean.fileSize;
}
}
if (mBean.fileSize < 1) {
new Thread(new Runnable() {
@Override
public void run() {
boolean flag = false;
synchronized (lock_getFileSize) {
flag = getFileSizeByNetwork(mBean);
}
if (flag) {
mState = DownloadConstants.DOWNLOAD_STATE_WAITTING;
} else {
Log.e(TAG, "get file size from network error 2");
}
}
}).start();
} else {
mState = DownloadConstants.DOWNLOAD_STATE_WAITTING;
}
} else {
callBackError("getDownloaderInfoFromDB Error,May be EngineDBOperator is Null.");
throw new DownloaderErrorException(
"getDownloaderInfoFromDB Error,May be EngineDBOperator is Null.");
}
}
/**
* @Title: getFileSizeByNetwork
* @Description: get file size from network and update listBeans
* @param bean
* @return
* @return: boolean
*/
private boolean getFileSizeByNetwork(DownloadBean bean) {
// HttpURLConnection connection = null;
long fileSize = bean.fileSize;
try {
//resolve android.os.NetworkOnMainThreadException
if (android.os.Build.VERSION.SDK_INT > 9) {
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
StrictMode.setThreadPolicy(policy);
}
String terminalId = AppStorePreference.getTerminalID(mContext);
JSONObject downloadJson = ApiService.getDownloadFileJson(terminalId, bean.downloadId);
//get file size from network if file size is not initialized
if (fileSize <= 0) {
// HttpClient client = new DefaultHttpClient();
HttpClient client = AsyncHttpClient.getDefaultHttpClient();//https request
HttpPost request = new HttpPost(bean.url);
request.setHeader("Content-Type", "application/json;charset=UTF8");
request.setEntity(new StringEntity(downloadJson.toString()));
HttpResponse response = client.execute(request);
int resopnseCode = response.getStatusLine().getStatusCode();
if (resopnseCode != 200 && resopnseCode != 206) {
callBackError("http return code error:" + resopnseCode);
return false;
}
// get file size
fileSize = response.getEntity().getContentLength();
mBean.fileSize = fileSize;
if (fileSize <= 0) {
callBackError("Can't get file size from server:" + fileSize);
return false;
}
//there is no free space both in sdcard and internal
if (!hasSpaceInSDCard()) {
return false;
}
long range = fileSize / mThreadCount;
// update listBean
for (int i = 0; i < mThreadCount - 1; i++) {
DownloadBean subBean = mBeans.get(i);
subBean.fileSize = fileSize;
subBean.startPosition = i * range;
subBean.endPosition = (i + 1) * range - 1;
}
DownloadBean subBean = mBeans.get(mThreadCount - 1);
subBean.fileSize = fileSize;
subBean.startPosition = (mThreadCount - 1) * range;
subBean.endPosition = fileSize - 1;
// update database
if (mDBOper != null) {
mDBOper.updateTaskCompleteSize(mBeans, mBean.url);
} else {
callBackError("getFileSizeByNetwork error��Maybe EngineDBOperator is Null.");
throw new DownloaderErrorException(
"getFileSizeByNetwork error��Maybe EngineDBOperator is Null.");
}
return true;
} else {// exit directly when file size > 0
return true;
}
} catch (Exception e) {
callBackError("Time out when get file size from server");
e.printStackTrace();
}
return false;
}
/**
* @Title: startDownloader
* @Description: start download, can invoke more than one time
* @throws DownloaderErrorException
* @return: void
*/
public void startDownloader() throws DownloaderErrorException {
//exit when is downloading
if (mState == DownloadConstants.DOWNLOAD_STATE_DOWNLOADING) {
return;
}
if (mBean == null) {
callBackError("Downloader is not initialized.");
return;
}
File file = new File(mBean.savePath);
File parentDirectory = file.getParentFile();
if (!parentDirectory.exists()) {
parentDirectory.mkdirs();
}
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
initBeanInDataBase();
//Forbade for clearing the mbeans list, when error occurred, but restart the task, so to reinitialize mBeans
if (mBeans.size() < 1) {
try {
addDownloaderInfoToDB(mBean);
} catch (DownloaderErrorException e) {
e.printStackTrace();
return;
}
}
/**
* start to download only when got file size
*/
synchronized (lock_getFileSize) {
//get file size error, try it again.
if (mState == DownloadConstants.DOWNLOAD_STATE_INIT) {
boolean flag = getFileSizeByNetwork(mBean);
if (!flag) {
callBackError("get file size error");
return;
}
}
//add new by teddy on 6th December start
if (mState == DownloadConstants.DOWNLOAD_STATE_PAUSE) {
if (mDBOper.isHasDownloadTaskByUrl(mBean.url, mBean.downloadId)) {
try {
getDownloaderInfoFromDB(mBean);
} catch (DownloaderErrorException e) {
e.printStackTrace();
}
}
}
//add new by teddy at 6th December end
}
mState = DownloadConstants.DOWNLOAD_STATE_DOWNLOADING;
mDBOper.removePauseFileByUrl(mBean.url, mBean.downloadId);// remove from pause table
mDoneThreadCount = 0;// ��ʼ������߳���
for (DownloadBean bean : mBeans) {
//the thread belongs to unfinished task
if (bean.currentPosition < (bean.endPosition - bean.startPosition)) {
HamalThread hamalThread = new HamalThread(bean);
hamalThread.start();
} else {// no need to recreate thread when it is finished
mDoneThreadCount++;
}
}
//complete download
if (mDoneThreadCount == mThreadCount) {
downloaderDone();
}
}
private class HamalThread extends Thread {
private int threadId;
private long startPos;
private long endPos;
private long compeleteSize;
private String urlstr;
private String downloadId;
private JSONObject downloadJson;
public HamalThread(DownloadBean bean) {
this.threadId = bean.threadId;
this.startPos = bean.startPosition;
this.endPos = bean.endPosition;
this.compeleteSize = bean.currentPosition;
this.urlstr = bean.url;
this.downloadId = bean.downloadId;
String terminalId = AppStorePreference.getTerminalID(mContext);
this.downloadJson = ApiService.getDownloadFileJson(terminalId, downloadId);
}
@Override
public void run() {
RandomAccessFile randomAccessFile = null;
InputStream is = null;
try {
// HttpClient client = new DefaultHttpClient();
HttpClient client = AsyncHttpClient.getDefaultHttpClient();//https request
HttpPost request = new HttpPost(urlstr);
request.setHeader("Content-Type", "application/json;charset=UTF8");
request.setEntity(new StringEntity(downloadJson.toString()));
//multi threads download
if (mThreadCount > 1) {
// set range (Range��bytes x-y)
Header headersize = new BasicHeader("Range", "bytes=" + (startPos + compeleteSize) + "-" + endPos);
request.addHeader(headersize);
Logger.d(headersize.getValue());
}
HttpResponse response = client.execute(request);
randomAccessFile = new RandomAccessFile(mBean.savePath, "rwd");
randomAccessFile.seek(startPos + compeleteSize);
// write downloaded file to the folder
is = new BufferedInputStream(response.getEntity().getContent());
Logger.d("sub thread get block size:" + response.getEntity().getContentLength());
byte[] buffer = new byte[bufferSize];
int length = -1;
DownloadUtil eUtil = DownloadUtil.getInstance();
//network is 3G
if (DownloadVariable.SUPPORT_NETWORK_TYPE == DownloadConstants.DOWNLOAD_NETWORK_ONLYWIFI) {
//network type is not WIFI
if (eUtil.getNetworkType() != DownloadConstants.NETWORK_STATE_WIFI) {
interruptDownloader();
return;
}
}
while ((length = is.read(buffer)) != -1) {
// Network checking
if (DownloadVariable.SUPPORT_NETWORK_TYPE == DownloadConstants.DOWNLOAD_NETWORK_ONLYWIFI) {
if (eUtil.getNetworkType() != DownloadConstants.NETWORK_STATE_WIFI) {
interruptDownloader();
return;
}
}
randomAccessFile.write(buffer, 0, length);
compeleteSize += length;
synchronized (lock_refresh_progress) {
mBean.currentPosition += length;
}
// update download information to database
mDBOper.updateTaskCompleteSize(threadId, compeleteSize,
urlstr, downloadId);
// stop
if (mState == DownloadConstants.DOWNLOAD_STATE_PAUSE
|| mState == DownloadConstants.DOWNLOAD_STATE_INTERRUPT
|| mState == DownloadConstants.DOWNLOAD_STATE_STOP
|| mState == DownloadConstants.DOWNLOAD_STATE_ERROR) {
return;
}
}
// sub thread downloading finished
mDoneThreadCount++;
} catch (Exception e) {
Log.e(TAG, "Connection is interrupted while downloading...");
interruptDownloader();
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
randomAccessFile.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (mDoneThreadCount == mThreadCount) {
downloaderDone();
}
}
}
/**
* @Title: getProgress
* @Description: get download progress
* @return
* @return: int
*/
public int getProgress() {
if (mBean == null || mBean.fileSize < 1) {
return 0;
}
return (int) (mBean.currentPosition * 100 / mBean.fileSize);
}
/**
* pause download
*/
public void pauseDownloader() {
try {
mState = DownloadConstants.DOWNLOAD_STATE_PAUSE;
if (null != mBean) {
mDBOper.addPauseFile(mBean.url, mBean.packageName, mBean.fileId, mBean.downloadId);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* pause download��not by user��
*/
private void interruptDownloader() {
mState = DownloadConstants.DOWNLOAD_STATE_INTERRUPT;
}
/**
* finish download
*/
public void stopDownloader() {
mState = DownloadConstants.DOWNLOAD_STATE_STOP;
mBean.currentPosition = 0;
removeDownloaderInfo(mBean.url, mBean.downloadId);
}
/**
* remove download info
*
* @param urlstr
*/
private void removeDownloaderInfo(String urlstr, String downloadId) {
mDBOper.deleteDownloadTaskByUrl(urlstr, downloadId);
mDBOper.removePauseFileByUrl(urlstr, downloadId);
mBeans.clear();
}
/**
* download done
*/
private void downloaderDone() {
mState = DownloadConstants.DOWNLOAD_STATE_DONE;
mBean.doneTime = System.currentTimeMillis();
mCallback.callbackWhenDownloadTaskListener(mState, mBean,
mBean.fileName + "download successfull");
removeDownloaderInfo(mBean.url, mBean.downloadId);
//save finished info save to database
mDBOper.addCompleteTask(mBean);
}
/**
* call back when error occurred
*
* @param info
*/
private void callBackError(String info) {
mState = DownloadConstants.DOWNLOAD_STATE_ERROR;
mCallback.callbackWhenDownloadTaskListener(mState, mBean, info);
removeDownloaderInfo(mBean.url, mBean.downloadId);
}
/**
* Check if there is enough space
*/
private boolean hasSpaceInSDCard() {
/*if (mBean.fileSize > DownloadUtil.getInstance().getFreeSpaceAtDirectory(
Environment.getExternalStorageDirectory().getAbsolutePath())) {*/
if (mBean.fileSize > DownloadUtil.getInstance().getFreeSpaceAtDirectory(mBean.savePath)) {
callBackError("There is no enough space.");
return false;
}
return true;
}
}