Android实现网络下载一(单任务下载–支持断点续传)
说起下载,在Android的一些类似游戏宝的项目中会频繁使用,比如说需求要下载一个apk文件,并且要支持暂停、继续等要求。当然在GitHib上也有一些优秀的下载框架可以供我们使用,但是更多时候的一些特殊需求可能要手动实现,所以类似的下载功能还是自己实现比较好。
附上demo源码,GitHub代码后续上传,这里的链接还是csdn的。
点这里下载源码,快,戳我戳我…
q:486789970
email:mr.cai_cai@foxmail.com
下图是一个单任务下载的动态图:
效果图如下(多任务下载将在下篇文章中实现)
上面的效果图就是单任务下载,支持断点续传、实时进度更新、下载暂停、下载继续,下载完成自动安装等功能;同时包括网络下载请求和本地文件的存储。
上图下载通过Service端下载,使用广播通知给activity更新UI界面
下载流程如下:
- 将下载URL和文件名等信息赋值给info对象
- 通过Intent将info对象传递给Service,使用子线程联网获取文件长度
- 拿到长度后启动下载任务类,并将下载信息传递过去
- 下载任务类中保存下载URL到数据库(为断点续传做准备)后开始下载
- 下载任务执行完成后使用广播通知activty界面进度条更新。
说的比较详细,其实很简单,看看代码就知道啦。往下看:
先是xml和activity
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.MainActivity">
<ProgressBar
android:id="@+id/probar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="30dp"
android:padding="10dp"
android:max="100" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:weightSum="3"
android:orientation="horizontal">
<TextView
android:id="@+id/txt_filename"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:gravity="center"
android:textSize="15dp"
android:textColor="#000000"
android:text="文件名" />
<Button
android:id="@+id/bt_start"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="15dp"
android:textColor="#000000"
android:text="开始" />
<Button
android:id="@+id/bt_stop"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="15dp"
android:textColor="#000000"
android:text="暂停" />
</LinearLayout>
</RelativeLayout>
package com.cc.downloaddemo.activity;
import android.Manifest;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.cc.downloaddemo.R;
import com.cc.downloaddemo.info.FileInfo;
import com.cc.downloaddemo.services.DownloadService;
/**
* 实现断点续传的Activity
* @author
*/
public class MainActivity extends Activity implements View.OnClickListener {
//需要动态获取的权限
private static final int REQUEST_EXTERNAL_STORAGE = 1;
private static String[] PERMISSIONS_STORAGE = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
// http://tfyxb2017-1251304591.file.myqcloud.com/qsfz/qsfz_mzyw_1663647040.apk
// http://down.shouji.kuwo.cn/star/mobile/kwplayer_ar_pcguanwangmobile.apk
//默认下载一个apk包
private static final String URL = "http://down.shouji.kuwo.cn/star/mobile/kwplayer_ar_pcguanwangmobile.apk";
//上下文
public final Activity activity = MainActivity.this;
//显示下载进度
private ProgressBar probar;
//文件名
private TextView txt_filename;
//开始暂停按钮
private Button bt_start, bt_stop;
//定义两个info实体类
FileInfo fileInfo;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//获取权限
MainActivity.verifyStoragePermissions(activity);
//获取控件
initView();
//初始化数据
initData();
}
/**
* 给实体类添加一些数据
*/
private void initData() {
//添加静态数据
fileInfo = new FileInfo(0, URL, "酷我音乐", 0, 0);
txt_filename.setText(fileInfo.getFilename());
}
/**
* 实例化控件
*/
private void initView() {
txt_filename = findViewById(R.id.txt_filename);
probar = findViewById(R.id.probar);
bt_start = findViewById(R.id.bt_start);
bt_stop = findViewById(R.id.bt_stop);
bt_start.setOnClickListener(this);
bt_stop.setOnClickListener(this);
//注册广播接收器,并设置flag(更新)
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(DownloadService.ACTION_UPDATE);
registerReceiver(broadcastReceiver, intentFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
//销毁广播
unregisterReceiver(broadcastReceiver);
}
/**
* 点击事件
* @param v
*/
@Override
public void onClick(View v) {
int temdId = v.getId();
if(temdId == R.id.bt_start){
//通知Service开始下载,设置flag(开始),并将下载文件传过去
//这里注意,服务需要在xml里面注册
Intent intent = new Intent(activity, DownloadService.class);
intent.setAction(DownloadService.ACTION_START);
intent.putExtra("fileInfo", fileInfo);
startService(intent);
}else if(temdId == R.id.bt_stop){
bt_start.setText("继续");
//通知Service暂停或者继续下载,并设置Stop(暂停或者继续),并将下载文件传过去
Intent intent = new Intent(activity, DownloadService.class);
intent.setAction(DownloadService.ACTION_STOP);
intent.putExtra("fileInfo", fileInfo);
startService(intent);
}
}
/**
* 定义广播更新UI
*/
BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
//拿到广播通知的更新tag,并获取到当前下载的长度百分比,显示在下载进度条上
if(intent.getAction().equals(DownloadService.ACTION_UPDATE)){
int finish = intent.getIntExtra("finished", 0);
probar.setProgress(finish);
//当finish为100时,说明已经全部下载结束。
if(finish == 100){
Toast.makeText(getApplicationContext(), fileInfo.getFilename() + "下载完成", Toast.LENGTH_SHORT).show();
bt_start.setVisibility(View.INVISIBLE);
bt_stop.setText("下载完成");
}
}
}
};
/**
* 验证读写权限
* @param activity
*/
public static void verifyStoragePermissions(Activity activity) {
// Check if we have write permission
int permission = ActivityCompat.checkSelfPermission(activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (permission != PackageManager.PERMISSION_GRANTED) {
// We don't have permission so prompt the user
ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE,
REQUEST_EXTERNAL_STORAGE);
}
}
}
注:在activity中记得申请读写权限,避免报错;
下来就是Service的:
package com.cc.downloaddemo.services;
import android.app.Service;
import android.content.Intent;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.support.annotation.Nullable;
import android.util.Log;
import com.cc.downloaddemo.info.FileInfo;
import java.io.File;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* 下载service
*/
public class DownloadService extends Service {
//通过URL地址下载apk文件并保存在本地存储路径
public static final String DOWNLOAD_PATH =
Environment.getExternalStorageDirectory().getAbsolutePath() +
"/downloads/";
//开始下载
public static final String ACTION_START = "ACTION_START";
//暂停下载
public static final String ACTION_STOP = "ACTION_STOP";
//更新下载进度
public static final String ACTION_UPDATE = "ACTION_UPDATE";
//定义handler的flag
public static final int MSG_INIT = 0;
//异步下载任务类
private DownloadTask downloadTask = null;
/**
* 执行下载、暂停、继续下载
* @param intent
* @param flags
* @param startId
* @return
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//获取从Activity传过去的参数,判断intent的值。
if(ACTION_START.equals(intent.getAction())){
FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
//启动初始化子线程,联网下载文件内容。
new InitThread(fileInfo).start();
} else if (ACTION_STOP.equals(intent.getAction())) {
//暂停下载时,更改下载任务类的flag便会自动暂停。
if(downloadTask != null){
downloadTask.ispause = true;
}
}
return super.onStartCommand(intent, flags, startId);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
/**
* 网络内容下载完成后
* 通过handler启动异步任务类开始正式下载文件。
*/
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
//是否为初始化消息
if(msg.what == MSG_INIT){
//获取发回来的信息
FileInfo fileInfo = (FileInfo) msg.obj;
//启动一个下载任务,将文件内容传递过去。
downloadTask = new DownloadTask(DownloadService.this, fileInfo);
//启动下载方法
downloadTask.download();
}
}
};
/**
* 定义子线程用来下载
*/
class InitThread extends Thread{
//定义一个文件用来接收下载信息
private FileInfo mFileInfo = null;
//启动线程时传入的对象
public InitThread(FileInfo mFileInfo) {
this.mFileInfo = mFileInfo;
}
@Override
public void run() {
//定义conn
HttpURLConnection conn = null;
//定义文件内容访问类
RandomAccessFile raf = null;
try {
//连接网络文件
URL url = new URL(mFileInfo.getUrl());
conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(3000);
conn.setRequestMethod("GET");
//自定义一个长度,注意尽量用long(处理下载消息进度百分比时好计算)
long length = -1;
//下载完成
if(conn.getResponseCode() == HttpURLConnection.HTTP_OK ){
//获取文件总长度赋值给length
length = conn.getContentLength();
}
//长度不能小于0
if(length <= 0){
return;
}
//判断该路径存在与否
File dir = new File(DOWNLOAD_PATH);
//文件不存在时创建
if(!dir.exists()){
dir.mkdir();
}
//本地创建一个文件
File file = new File(dir, mFileInfo.getFilename());
//输出流
raf = new RandomAccessFile(file, "rwd");
//设置本地的文件长度(等于下载文件的总长度。杜绝浪费资源)
raf.setLength(length);
//赋值给定义的对象长度
mFileInfo.setLength(length);
//启动flag,将文件发送给handler处理
handler.obtainMessage(MSG_INIT, mFileInfo).sendToTarget();
}catch (Exception e){
e.printStackTrace();
}finally {
try {
//关闭资源
raf.close();
conn.disconnect();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
}
注:记得下载完之后关闭conn连接和流;
异步下载任务类(重中之重):
package com.cc.downloaddemo.services;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import com.cc.downloaddemo.db.dao.ThreadDao;
import com.cc.downloaddemo.db.impl.ThreadDaoImpl;
import com.cc.downloaddemo.info.FileInfo;
import com.cc.downloaddemo.info.ThreadInfo;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
/**
* 任务下载类
* @author
*/
public class DownloadTask {
//上下文
private Context context;
//定义数据库线程对象文件
private FileInfo fileInfo;
//实例化接口
public static ThreadDao threadDao = null;
//定义finished(当前已下载的长度)
private int finished = 0;
//默认直接下载
public boolean ispause = false;
/**
* 构造方法
* @param context
* @param fileInfo
*/
public DownloadTask(Context context, FileInfo fileInfo) {
this.context = context;
//拿到handler传过来的文件
this.fileInfo = fileInfo;
//实例化接口实现类
threadDao = new ThreadDaoImpl(context);
}
/**
* 下载方法
*/
public void download(){
//下载任务一开始,先查询数据库,看是否有文件在等待下载。
//读取数据库的所有线程信息
List<ThreadInfo> threads = threadDao.getThreads(fileInfo.getUrl());
//定义一个线程内容类
ThreadInfo threadInfo;
//当list的size为0时,说明没有等待下载的线程,为1时则有。
if(threads.size() == 0){
//开始初始化线程文件信息
threadInfo = new ThreadInfo(0, fileInfo.getUrl(), 0, (int)fileInfo.getLength(), 0);
}else{
Log.i("download", "threads = " + threads.size());
//因为一开始的静态内容只有一条,所以这里就直接拿下标为0的啦。
//正式开发中,如果有多条的话,需要循环取出
threadInfo = threads.get(0);
}
//创建子线程进行下载
new DownloadThread(threadInfo).start();
}
/**
* 下载线程
*/
class DownloadThread extends Thread{
//继续定义一个临时线程对象
private ThreadInfo threadInfo;
//构造
public DownloadThread(ThreadInfo threadInfo) {
this.threadInfo = threadInfo;
}
@Override
public void run() {
//当前线程信息在数据库没有时,说明是新线程下载,将新线程同步给数据库。
if(!threadDao.isExits(threadInfo.getUrl(), threadInfo.getId())){
threadDao.insertThread(threadInfo);
}
//定义conn、输入流和文件内容访问类
HttpURLConnection conn = null;
InputStream inputStream = null;
RandomAccessFile raf = null;
try{
//开始联网下载
URL url = new URL(threadInfo.getUrl());
conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(3000);
conn.setRequestMethod("GET");
//设置下载位置(通过当前开始值和现在值判断下载位置)
int start = threadInfo.getStart() + threadInfo.getFinished();
conn.setRequestProperty("Range", "bytes=" + start + "-" + threadInfo.getEnd());
//设置文件写入位置
File file = new File(DownloadService.DOWNLOAD_PATH, fileInfo.getFilename());
raf = new RandomAccessFile(file, "rwd");
raf.seek(start);
//定义一个广播,用来更新下载进度
Intent intent = new Intent(DownloadService.ACTION_UPDATE);
//
finished += threadInfo.getFinished();
//开始下载
if(conn.getResponseCode() == HttpURLConnection.HTTP_PARTIAL){
//读取数据
inputStream = conn.getInputStream();
byte[] buffer = new byte[1024 * 4];
int len = -1;
long time = System.currentTimeMillis();
while ((len = inputStream.read(buffer)) != -1){
//写入文件
raf.write(buffer, 0, len);
//把下载进度发送给广播
finished += len;
if(System.currentTimeMillis() - time > 500){
time = System.currentTimeMillis();
intent.putExtra("finished", (int)(finished / (float)fileInfo.getLength() * 100));
Log.i("cghangdu===", (int)(finished / (float)fileInfo.getLength() * 100) + "");
context.sendBroadcast(intent);
}
//在下载暂停时,保存下载进度
if(ispause){
threadDao.updateThread(threadInfo.getUrl(), threadInfo.getId(), finished);
Log.i("下载暂停", "finished---->" + finished);
return;
}
}
intent.putExtra("finished", 100);
context.sendBroadcast(intent);
//下载完成,删除数据库所保存的线程信息
threadDao.deleteThread(threadInfo.getUrl(), threadInfo.getId());
openFile(file);
}
}catch (Exception e){
e.printStackTrace();;
}finally {
conn.disconnect();
try {
inputStream.close();
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 自动安装
* @param file
*/
private void openFile(File file) {
// TODO Auto-generated method stub
Log.e("OpenFile", file.getName());
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(android.content.Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(file),
"application/vnd.android.package-archive");
context.startActivity(intent);
}
}
注:数据库代码在下面
先创建数据库:
package com.cc.downloaddemo.db;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
/**
* 数据库帮助类
* @author
*/
public class DBHelper extends SQLiteOpenHelper {
public static final int DB_VERSION = 1;
public static final String DB_NAME = "download.db";
public static final String DB_TABLE_NAME = "thread_info";
public static final String DB_CREATE_SQL = "create table " + DB_TABLE_NAME + "("
+ "_id integer primary key autoincrement,"
+ "thread_id Integer,"
+ "url text,"
+ "start Integer,"
+ "end Integer,"
+ "finished Integer)";
public static final String DB_DROP_SQL = "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(DB_CREATE_SQL);
}
@Override
public void onUpgrade(SQLiteDatabase db, int i, int i1) {
db.execSQL(DB_DROP_SQL);
db.execSQL(DB_CREATE_SQL);
}
}
再定义保存下载进度等接口:
package com.cc.downloaddemo.db.dao;
import com.cc.downloaddemo.info.ThreadInfo;
import java.util.List;
/**
* 线程数据访问接口
* @author
*/
public interface ThreadDao {
/**
* 插入一条线程信息
* @param threadInfo
*/
public void insertThread(ThreadInfo threadInfo);
/**
* 更新线程信息
* @param url
* @param thread_id
* @param finished
*/
public void updateThread(String url, int thread_id, int finished);
/**
* 删除线程
* @param url
* @param thread_id
*/
public void deleteThread(String url, int thread_id);
/**
* 查询线程
* @param url
* @return
*/
public List<ThreadInfo> getThreads(String url);
/**
* 判断线程是否已存在
* @param url
* @param thread_id
* @return
*/
public boolean isExits(String url, int thread_id);
}
接口实现类:
package com.cc.downloaddemo.db.impl;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import com.cc.downloaddemo.db.DBHelper;
import com.cc.downloaddemo.db.dao.ThreadDao;
import com.cc.downloaddemo.info.ThreadInfo;
import java.util.ArrayList;
import java.util.List;
/**
* 线程数据接口实现类
* @author
*/
public class ThreadDaoImpl implements ThreadDao {
private DBHelper dbHelper = null;
public ThreadDaoImpl(Context context) {
dbHelper = new DBHelper(context);
}
/**
* 实现插入方法
* @param threadInfo
*/
@Override
public void insertThread(ThreadInfo threadInfo) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.execSQL("insert into thread_info(thread_id, url, start, end, finished) values(?, ?, ?, ?, ?)",
new Object[]{threadInfo.getId(), threadInfo.getUrl(), threadInfo.getStart(), threadInfo.getEnd(), threadInfo.getFinished()});
db.close();
}
/**
* 更新
* @param url
* @param thread_id
* @param finished
*/
@Override
public void updateThread(String url, int thread_id, int finished) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("finished", finished);
long ret = -1;
do {
ret = db.update("thread_info", values, "thread_id = ? and url = ?",
new String[] { thread_id + "", url });
} while (ret < 0);
}
@Override
public void deleteThread(String url, int thread_id) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.execSQL("delete from thread_info");
db.close();
}
@Override
public List<ThreadInfo> getThreads(String url) {
List<ThreadInfo> infos = new ArrayList<>();
SQLiteDatabase db = dbHelper.getWritableDatabase();
Cursor cursor = db.rawQuery("select * from thread_info where url = ?", new String[]{url});
while (cursor.moveToNext()){
ThreadInfo info = new ThreadInfo();
info.setId(cursor.getInt(cursor.getColumnIndex("thread_id")));
info.setUrl(cursor.getString(cursor.getColumnIndex("url")));
info.setStart(cursor.getInt(cursor.getColumnIndex("start")));
info.setEnd(cursor.getInt(cursor.getColumnIndex("end")));
info.setFinished(cursor.getInt(cursor.getColumnIndex("finished")));
infos.add(info);
}
db.close();
return infos;
}
@Override
public boolean isExits(String url, int thread_id) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
Cursor cursor =
db.rawQuery("select * from thread_info where url = ? and thread_id = ?", new String[]{url, thread_id + ""});
boolean exits = cursor.moveToNext();
cursor.close();
db.close();
return exits;
}
}
注:实体类在最后面:
两个实体类:
package com.cc.downloaddemo.info;
import java.io.Serializable;
/**
* 本地存储文件信息
* @author
*/
public class FileInfo implements Serializable {
//id
private int id;
//url
private String url;
//文件名
private String filename;
//长度
private long length;
//下载了多少
private int finished;
public FileInfo() {
super();
}
public FileInfo(int id, String url, String filename, long length, int finished) {
this.id = id;
this.url = url;
this.filename = filename;
this.length = length;
this.finished = finished;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
public int getFinished() {
return finished;
}
public void setFinished(int finished) {
this.finished = finished;
}
@Override
public String toString() {
return "FileInfo{" +
"id=" + id +
", url='" + url + '\'' +
", filename='" + filename + '\'' +
", length=" + length +
", finished=" + finished +
'}';
}
}
第二个:
package com.cc.downloaddemo.info;
/**
* 数据库线程信息
* @author
*/
public class ThreadInfo {
//id
private int id;
//url
private String url;
//从哪里下载
private int start;
//结束
private int end;
//下载了多少
private int finished;
public ThreadInfo() {
super();
}
public ThreadInfo(int id, String url, int start, int end, int finished) {
this.id = id;
this.url = url;
this.start = start;
this.end = end;
this.finished = finished;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public int getStart() {
return start;
}
public void setStart(int start) {
this.start = start;
}
public int getEnd() {
return end;
}
public void setEnd(int end) {
this.end = end;
}
public int getFinished() {
return finished;
}
public void setFinished(int finished) {
this.finished = finished;
}
@Override
public String toString() {
return "ThreadInfo{" +
"id=" + id +
", url='" + url + '\'' +
", start=" + start +
", end=" + end +
", finished=" + finished +
'}';
}
}
以上就是所有的代码
附上demo源码,GitHub代码后续上传,这里的链接还是csdn的。
点这里下载源码,快,戳我戳我…
q:486789970
email:mr.cai_cai@foxmail.com
如果有什么问题,欢迎大家指导。并相互联系,希望能够通过文章互相学习。
---财财亲笔