先上代码
效果图
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private TextView textView;
private Button mBtnstart;
private Button mBtnstop;
private ProgressBar progressBar;
//url必须带前缀,也就是协议,否则会报错
public static final String url = "http://www.imooc.com/mobile/imooc.apk";
private FileInfo fileInfo;
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if(DownloadService.ACTION_UPDATE.equals(action)){
int finished = intent.getIntExtra("finished", 0);
progressBar.setProgress(finished);
textView.setText("下载进度"+String.valueOf(finished)+"%");
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView) findViewById(R.id.textView);
mBtnstart = (Button) findViewById(R.id.btn_start);
mBtnstop = (Button) findViewById(R.id.btn_stop);
mBtnstop.setOnClickListener(this);
mBtnstart.setOnClickListener(this);
progressBar = (ProgressBar) findViewById(R.id.progressBar);
progressBar.setMax(100);
fileInfo = new FileInfo(0,url,"imooc.apk",0,0);
//注册广播
IntentFilter filter = new IntentFilter(DownloadService.ACTION_UPDATE);
registerReceiver(receiver, filter);
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.btn_start:
Intent intent = new Intent(this, DownloadService.class);
intent.setAction(DownloadService.ACTION_START);
intent.putExtra("fileinfo",fileInfo);
startService(intent);
break;
case R.id.btn_stop:
Intent intent2 = new Intent(this, DownloadService.class);
intent2.setAction(DownloadService.ACTION_STOP);
startService(intent2);
break;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(receiver);
}
}
public class DownloadService extends Service {
private static final String TAG = "DownloanService";
public static final String ACTION_START = "ACTION_START";
public static final String ACTION_STOP = "ACTION_STOP";
public static final String ACTION_UPDATE = "ACTION_UPDATE";
public static final String DOWNLOAD_PATH = Environment.getExternalStorageDirectory()
.getAbsolutePath()+"/downloads/";
public static final int MSG_INIT_THREAD = 0;
private DownloadTask mTask = null;
public static boolean isStarted = false;
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what){
case MSG_INIT_THREAD:
FileInfo fileInfo = (FileInfo) msg.obj;
Log.d(TAG, "length: "+fileInfo.length);
//将文件信息传给下载任务
mTask = new DownloadTask(DownloadService.this,fileInfo);
mTask.download();//启动任务下载
break;
}
}
};
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if(ACTION_START.equals(intent.getAction())){
//获取文件信息
FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileinfo");
if(!isStarted) {
new initThread(fileInfo).start();
isStarted = true;
}
}else if(ACTION_STOP.equals(intent.getAction())){
if(mTask!=null){
mTask.isPause = true;
isStarted = false;
}
}
return super.onStartCommand(intent, flags, startId);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
class initThread extends Thread{
private FileInfo fileInfo;
private RandomAccessFile raf;
private HttpURLConnection conn;
public initThread(FileInfo fileInfo){
this.fileInfo = fileInfo;
}
@Override
public void run() {
super.run();
try {
URL url = new URL(fileInfo.url);//www.imooc.com/mobile/imooc.apk
conn = (HttpURLConnection) url.openConnection();//打开链接
conn.setConnectTimeout(3000);
conn.setReadTimeout(3000);
int len = -1;
if(conn.getResponseCode() == 200){
len = conn.getContentLength();//获取服务器文件长度
}
if(len < 0){
return;
}
File dir = new File(DOWNLOAD_PATH);
if(!dir.exists()){
dir.mkdir();//目录不存在创建目录
}
File file = new File(dir,fileInfo.fileName);
//在指定路径下创建一个个服务器文件大小一样的文件
raf = new RandomAccessFile(file,"rwd");
raf.setLength(len);//设置临时文件的长度为服务器文件长度
fileInfo.length = len;//设置文件信息
handler.obtainMessage(MSG_INIT_THREAD,fileInfo).sendToTarget();
}catch (Exception e){
e.printStackTrace();
}finally {
if(conn!=null)
conn.disconnect();
try {
if(raf!=null)
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
public class DownloadTask {
private Context mContext;
//要下载的文件信息,
private FileInfo fileInfo;
private ThreadDao dao;
public boolean isPause = false;
//完成的进度
private int finished;
public DownloadTask(Context mContext, FileInfo fileInfo) {
this.mContext = mContext;
this.fileInfo = fileInfo;
dao = new ThreadDaoImpl(mContext);
}
public void download(){
//每次下载任务前,根据url查询线程信息
List<ThreadInfo> threadInfos = dao.queryThread(fileInfo.url);
ThreadInfo threadInfo;
if(threadInfos.size() == 0){//第一次,创建文件信息
threadInfo = new ThreadInfo(0,0,fileInfo.length,fileInfo.url,0);
}else{
//以后从集合中取出文件信息
threadInfo = threadInfos.get(0);
}
new DownloadThread(threadInfo).start();
}
class DownloadThread extends Thread{
private ThreadInfo threadinfo;
private RandomAccessFile raf;
private HttpURLConnection conn;
public DownloadThread(ThreadInfo threadInfo){
this.threadinfo = threadInfo;
}
@Override
public void run() {
super.run();
//第一次数据库中不存在信息,向数据库写入信息
if(!dao.isExists(threadinfo.url, threadinfo.id)){
dao.insertThread(threadinfo);
}
//设置下载位置
try {
URL url = new URL(threadinfo.url);//下载链接
conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(5000);
conn.setConnectTimeout(4000);
//当前下载位置等于起始位置加上已经下载的进度
int start = threadinfo.start+ threadinfo.finished;
//下载的范围为起始位置到文件长度,因为是单线程下载
conn.setRequestProperty("Range","byte = "+start+"-"+ threadinfo.end);
File file = new File(DownloadService.DOWNLOAD_PATH,fileInfo.fileName);
raf = new RandomAccessFile(file,"rwd");
raf.seek(start);//指定从某个位置起
Intent intent = new Intent(DownloadService.ACTION_UPDATE);
finished += threadinfo.finished;//更新完成的进度
//开始下载
if(conn.getResponseCode() == 200){
//读取数据
int len = -1;
long time = System.currentTimeMillis();
InputStream stream = conn.getInputStream();
byte[] buffer = new byte[1024<<2];//每次赌徒多少个字节
while ((len = stream.read(buffer))!=-1){
//写入文件
raf.write(buffer,0,len);
finished += len;
if(System.currentTimeMillis() - time >200) {
time = System.currentTimeMillis();
//通知Activity更新进度条
intent.putExtra("finished", finished *100/ threadinfo.end);
mContext.sendBroadcast(intent);
}
//下载暂停,保存进度到数据库
if(isPause){
//将当前的信息保存到数据库
dao.updateThread(threadinfo.url, threadinfo.id+"",finished);
return;
}
}
//下载完成删除删除下载信息
dao.deleteThread(threadinfo.url, threadinfo.id+"");
}
} catch (Exception e) {
e.printStackTrace();
dao.updateThread(threadinfo.url, threadinfo.id+"",finished);
DownloadService.isStarted = false;
}finally {
{
conn.disconnect();
try {
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}
public class DBHelper extends SQLiteOpenHelper {
public static final String DB_NAME = "download.db";
public static final int DB_VERSION = 1;
//id 必须是integer,否则会报错
public static final String CREATE_TABLE = "create table thread_info(id integer primary key autoincrement," +
"thread_id integer,url text,start integer,end integer,finished integer)";
public static final String DROP_TABLE = "drop table if exists thread_info";
public DBHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL(DROP_TABLE);
db.execSQL(CREATE_TABLE);
}
}
public class FileInfo implements Serializable{
public int id;
public String url;
public String fileName;
public int finished;
public int length;
public FileInfo() {
}
public FileInfo(int id, String url, String fileName, int finished, int length) {
this.id = id;
this.url = url;
this.fileName = fileName;
this.finished = finished;
this.length = length;
}
@Override
public String toString() {
return "FileInfo{" +
"id=" + id +
", url='" + url + '\'' +
", fileName='" + fileName + '\'' +
", finished=" + finished +
", length=" + length +
'}';
}
}
public class ThreadInfo {
public int id;
public int start;
public int end;
public String url;
public int finished;
public ThreadInfo() {
}
public ThreadInfo(int id, int start, int end, String url,int finished) {
this.id = id;
this.start = start;
this.end = end;
this.url = url;
this.finished = finished;
}
@Override
public String toString() {
return "ThreadInfo{" +
"id=" + id +
", start=" + start +
", end=" + end +
", url='" + url + '\'' +
'}';
}
}
public interface ThreadDao {
void insertThread(ThreadInfo threadInfo);
void deleteThread(String url,String thread_id);
void updateThread(String url,String thread_id,int finished);
List<ThreadInfo> queryThread(String url);
boolean isExists(String url,int thread_id);
}
public class ThreadDaoImpl implements ThreadDao {
DBHelper dbHelper;
public ThreadDaoImpl(Context context){
dbHelper = new DBHelper(context);
}
@Override
public void insertThread(ThreadInfo threadInfo) {
SQLiteDatabase db = dbHelper.getReadableDatabase();
ContentValues values = new ContentValues();
values.put("thread_id",threadInfo.id);
values.put("url",threadInfo.url);
values.put("start",threadInfo.start);
values.put("end",threadInfo.end);
values.put("finished",threadInfo.finished);
db.insert("thread_info",null,values);
db.close();
}
@Override
public void deleteThread(String url, String thread_id) {
SQLiteDatabase db = dbHelper.getReadableDatabase();
int count = db.delete("thread_info","url = ? and thread_id = ?",new String[]{url,thread_id} );
System.out.println("========count: "+count);
db.close();
}
@Override
public void updateThread(String url, String thread_id, int finished) {
SQLiteDatabase db = dbHelper.getReadableDatabase();
ContentValues values = new ContentValues();
values.put("finished",finished);
db.update("thread_info",values,"url = ? and thread_id = ?",new String[]{url,thread_id});
}
@Override
public List<ThreadInfo> queryThread(String url) {
List<ThreadInfo> list = new ArrayList<>();
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = db.query("thread_info",null,"url = ?",
new String[]{url},null,null,null);
while (cursor.moveToNext()){
ThreadInfo threadInfo = new ThreadInfo();
threadInfo.id = cursor.getInt(cursor.getColumnIndex("thread_id"));
threadInfo.url= cursor.getString(cursor.getColumnIndex("url"));
threadInfo.start = cursor.getInt(cursor.getColumnIndex("start"));
threadInfo.end = cursor.getInt(cursor.getColumnIndex("end"));
threadInfo.finished = cursor.getInt(cursor.getColumnIndex("finished"));
list.add(threadInfo);
}
cursor.close();
db.close();
return list;
}
public boolean isExists(String url,int thread_id){
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = db.query("thread_info",null,"url = ? and thread_id = ?",
new String[]{url,thread_id+""},null,null,null);
boolean exists = cursor.moveToNext();
cursor.close();
db.close();
return exists;
}
}
总结
涉及到的知识点
Activity和Service交互:Activity通过Intent向Service传递,
Service通过广播,另外还可以选择Handler或者进程间通信方式
文件如何下载的;从数据库中取出文件上次下载的位置,使用Range属性去请求
//当前下载位置等于起始位置加上已经下载的进度
int start = threadinfo.start+ threadinfo.finished;
//下载的范围为起始位置到文件长度,因为是单线程下载
conn.setRequestProperty("Range","byte = "+start+"-"+ threadinfo.end);
使用RandomAccessFile的seek方法从指定位置开始写
raf = new RandomAccessFile(file,"rwd");
raf.seek(start);//指定从某个位置起
进度如何更新的:在下载文件过程中,通过广播把当前下载的进度发送给Activity,更新界面
如何断点续传的;
下载暂停的时候把进度保存到数据库中去,通过线程的url,id,
//下载暂停,保存进度到数据库
if(isPause){
//将当前的信息保存到数据库
dao.updateThread(threadinfo.url, threadinfo.id+"",finished);
return;
}
下次继续下载的时候从数据库中查找线程信息,接着上次进度下载
//每次下载任务前,根据url查询线程信息
List<ThreadInfo> threadInfos = dao.queryThread(fileInfo.url);
......
finished += threadinfo.finished;//更新完成的进度,必须要加上数据库中查询到的文件信息
Service在整个过程中起到什么作用?
其实在Activity中开启线程下载也是可以的,那我们为什么要使用Service呢?
因为Activity属于一个前台的组件,只要是前台组件就有可能被用户关闭,也有可能切换到后台,被系统回收,一旦Activity被回收,那么在Activity中启动的线程就无法跟踪,管理,会导致很多问题,没法对线程进行操作。
Service属于后台的组件,不是直接和用户交互的,用户没法直接关闭,Service优先级比较高,一般不会被系统回收,线程的操作放在Service里面比较保险。
最后给出源码下载位置
点此下载源码