最近一个在做项目需要用到断点续传的方式来更新apk,在网上也找到了一些例子,写的真心不错。
A.打开APP开启下载更新服务。
a.检查网络是否连接。若没有网络打开网络监听,在联网后进行下载;若已经连接就直接进行下载。
B.开启线程创建写入文件(从网上获取的文件字节将被写入此文件中)。
C.在创建文件完成后,开启线程进行文件下载并在数据库中记录下载文件的节点与此下载线程信息。
D.在下次开启下载任务时,从数据库中获取下载线程信息与节点信息,继续进行下载。
E.下载完成后进行安装,这里提供普通安装与静默安装两种安装方式。
以上即是我的断点续传的思路。
MainActivity.class
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
DownloadService.DOWNLOAD_PATH = getApplicationContext().getFilesDir()
.getAbsolutePath() + "/";
// 检查网络是否连接
isConnectNetWork();
}
/**
* 判断网络是否连接
*/
private void isConnectNetWork() {
if (NetworkUtil.isNetworkAvailable(getApplicationContext())) {
LogUtils.i(TAG, "网络已连接");
startDownLoadService();
} else {
LogUtils.i(TAG, "网络未连接");
registNetChangeReceiver();
}
}
/**
* 开始断点续传下载后台服务
*/
private void startDownLoadService() {
intent = new Intent(MainActivity.this, DownloadService.class);
intent.setAction(DownloadService.ACTION_DOWNLOAD);
startService(intent);
}
}
DownloadSerivce.class
//每个服务都只会存在一个实例
public class DownloadService extends Service {
private static final String TAG = DownloadService.class.getSimpleName();
public static final String ACTION_DOWNLOAD = "DOWNLOAD";
public static final String ACTION_STOP = "STOP";
public static final String ACTION_UPDATE = "UPDATE";
/**
* 存放下载文件的文件夹路径
*/
public static String DOWNLOAD_PATH;
private static final int MSG_INIT = 0;// 代表创建本地文件完成
private DownloadTask mTask;
private DownloadTask mTask_TV1;
private DownloadTask mTask_TV2;
// 权健文件封装
private FileInfo fileInfo_apk;
private String apkUrl;
// TV1的文件封装对象
private FileInfo fileInfo_TV1;
// TV2的文件封装对象
private FileInfo fileInfo_TV2;
private DeviceApplicationDAO deviceApplicationDAO;
private List<FileInfo> fileInfos;
private List<DeviceApplicationDTO> deviceApplicationDTOs;
private ApkInfoDTO apkInfoDTO;
@Override
public void onCreate() {
deviceApplicationDAO = new DeviceApplicationDAOImpl(
MainActivity.context);
registNetChangeReceiver();
super.onCreate();
}
/**
* 在每次服务启动的时候调用 如果我们希望服务一旦启动就立刻去执行某个动作,就可以将逻辑写在onStartCommand()方法里
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null) {
// 获得Activity传来的参数
if (ACTION_DOWNLOAD.equals(intent.getAction())) {
if (mTask == null || mTask.isPause || mTask.isFinished) {
// 这里的判断语句要将mTask==null放在最前面,因为mTask还没有new出来
// 启动初始化线程
checkUpdate();
checkOtherApkViersion();
LogUtils.d(TAG, "这个接口被调用了1");
}
} else if (ACTION_STOP.equals(intent.getAction())) {
if (mTask != null && !mTask.isPause && !mTask.isFinished) {
// 这里的判断语句要将mTask!=null放在最前面,因为mTask还没有new出来
// FileInfo fileInfo = (FileInfo) intent
// .getSerializableExtra("fileInfo");
// Log.d("测试", "ACTION_STOP:" + fileInfo.toString());
if (mTask != null) {
mTask.isPause = true;
}
}
}
} else {
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private Handler mHandler = new Handler() {
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case MSG_INIT:
FileInfo fileInfo = (FileInfo) msg.obj;
Log.d("测试", "fileInfo.toString()------->" + fileInfo.toString())
// 启动下载任务
mTask = new DownloadTask(DownloadService.this, fileInfo);
mTask.download();
LogUtils.i(TAG, "----mTask----" + mTask.toString());
break;
}
}
};
/**
* 从网上读取文件的长度然后再本地建立文件
*/
private class InitThread extends Thread {
private FileInfo mFileInfo;
public InitThread(FileInfo fileInfo) {
mFileInfo = fileInfo;
}
public void run() {
Log.d("测试", "InitThread" + mFileInfo.getFileName());
HttpURLConnection connection = null;
RandomAccessFile raf = null;
try {
// 连接网络文件
LogUtils.d("测试", mFileInfo.getUrl());
URL url = new URL(mFileInfo.getUrl());
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(3000);// 设置连接超时
connection.setReadTimeout(3000);// 设置读取超时
connection.setRequestMethod("GET");
// 判断是否成功连接
int length = -1;
if (connection.getResponseCode() == HttpStatus.SC_OK) {
// 获得文件长度
length = connection.getContentLength();
LogUtils.d("InitThread", mFileInfo.getFileName() + "的长度为"
+ length);
}
if (length <= 0) {
return;
}
File dir = new File(DOWNLOAD_PATH);
// 判断存放下载文件的文件的文件夹是否存在
if (!dir.exists()) {
dir.mkdir();
}
// 在本地创建文件
File file = new File(dir, mFileInfo.getFileName() + ".apk");
// 随机存取文件,用于断点续传,r-读取/w-写入/d-删除权限
raf = new RandomAccessFile(file, "rwd");
// 设置本地文件长度
raf.setLength(length);
mFileInfo.setLength(length);
mHandler.obtainMessage(MSG_INIT, mFileInfo).sendToTarget();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
raf.close();
connection.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
private ConnectivityManager connectivityManager;
private NetworkInfo info;
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals("android.net.conn.CONNECTIVITY_CHANGE")) {
connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
info = connectivityManager.getActiveNetworkInfo();
if (info != null && info.isAvailable()) {
// 网络已经连接
if (DownloadTask.isPause) {
startDownLoadService();
}
} else {
DownloadTask.isPause = true;
}
}
}
};
/**
* 注册网络连接状态变化广播
*/
private void registNetChangeReceiver() {
IntentFilter filter = new IntentFilter();
filter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
registerReceiver(mReceiver, filter);
}
@Override
public void onDestroy() {
unregisterReceiver(mReceiver);
super.onDestroy();
}
/**
* 开始断点续传任务
*/
Intent intent;
/**
* 开始断点续传下载任务
*/
private void startDownLoadService() {
intent = new Intent(MainActivity.context, DownloadService.class);
intent.setAction(DownloadService.ACTION_DOWNLOAD);
startService(intent);
}
/**
* 检查apk版本(这里我使用了Volley作为网络请求,主要是获得更新的apk信息)
*/
private void checkOtherApkViersion() {
try {
StringRequest stringRequest = new StringRequest(
URLControlOfficial.getOtherApkVersion, new Listener<String>() {
@Override
public void onResponse(String response) {
LogUtils.i(TAG, "检查apk版本------->" + response);
OutOtherApkGsonDTO<ApplicationDTO> outOtherApkGsonDTO = new Gson()
.fromJson(
response,
new TypeToken<OutOtherApkGsonDTO<ApplicationDTO>>() {
}.getType());
if (outOtherApkGsonDTO.getTotal() == 0) {
LogUtils.i(TAG, "沒有可以更新apk");
} else if (outOtherApkGsonDTO.getTotal() > 0) {
LogUtils.i(TAG, "有可以更新apk");
try {
deviceApplicationDTOs = new ArrayList<DeviceApplicationDTO>();
for (int i = 0; i < outOtherApkGsonDTO
.getApplication().size(); i++) {
DeviceApplicationDTO deviceApplicationDTO = new DeviceApplicationDTO();
deviceApplicationDTO.setId(i);
deviceApplicationDTO
.setActivity(outOtherApkGsonDTO
.getApplication()
.get(i)
.getApplicationFlagID());
deviceApplicationDTO
.setApkPackage(outOtherApkGsonDTO
.getApplication()
.get(i)
.getDeviceApplication()
.getApkPackage());
deviceApplicationDTO
.setApkName(outOtherApkGsonDTO
.getApplication()
.get(i)
.getDeviceApplication()
.getApkName());
deviceApplicationDTO
.setApkUrl(outOtherApkGsonDTO
.getApplication()
.get(i)
.getDeviceApplication()
.getApkUrl());
deviceApplicationDTOs
.add(deviceApplicationDTO);
}
// 如果沒有的此apk的話,就插入数据库
try {
// 向数据库中插入线程信息
if (!deviceApplicationDAO.isExists(deviceApplicationDTOs
.get(0).getApkUrl())) {
deviceApplicationDAO
.insertDeviceApplicationDTO(deviceApplicationDTOs
.get(0));
fileInfo_TV1 = new FileInfo(1, deviceApplicationDAO
.getDeviceApplicationDTO("TV1").getApkUrl(),
deviceApplicationDAO.getDeviceApplicationDTO("TV1")
.getApkName());
LogUtils.i(TAG, "开始创建相应的apk文件");
new InitThread(fileInfo_TV1).start();
}
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}, new ErrorListenerCallBack());
VolleyController.getInstance()
.getRequestQueue(MainActivity.context).add(stringRequest);
} catch (Exception e) {
e.printStackTrace();
}
}
}
DownTask,clas
/**
* 下载任务类
*
*/
public class DownloadTask {
private static final String TAG = "DownloadTask";
private Context mContext;
private FileInfo mFileInfo;
private ThreadDAO mDAO;
private long mFinished;// 用于更新UI的下载进度
public static boolean isPause = true;// 判断是否正在下载
public boolean isFinished;// 判断是否下载完成
private long newVersionCode;
private String newApkName;
private DeviceApplicationDAO deviceApplicationDAO;
public DownloadTask(Context context, FileInfo fileInfo) {
super();
this.mContext = context;
this.mFileInfo = fileInfo;
mDAO = new ThreadDAOImpl(context);
deviceApplicationDAO = new DeviceApplicationDAOImpl(context);
isPause = false;
isFinished = false;
}
public void download() {
Log.d("测试", "download");
// 读取上次的线程信息
List<ThreadInfo> list = mDAO.getThreads(mFileInfo.getUrl());
ThreadInfo threadInfo = null;
if (list.size() == 0 || list == null) {
// 有可能是第一次下载,数据库中还没有信息
threadInfo = new ThreadInfo(0, mFileInfo.getUrl(), 0,
mFileInfo.getLength(), 0, newVersionCode, newApkName);
} else {
threadInfo = mDAO.getThread(mFileInfo.getUrl());
}
new DownloadThread(threadInfo).start();
}
/**
* 下载线程
*
*/
class DownloadThread extends Thread {
private ThreadInfo mThreadInfo;
public DownloadThread(ThreadInfo ThreadInfo) {
mThreadInfo = ThreadInfo;
}
public void run() {
Log.d("测试", "DownloadThread" + mFileInfo.getFileName()
+ "----id-----" + mFileInfo.getId());
try {
// 向数据库中插入线程信息
if (!mDAO.isExists(mThreadInfo.getUrl(), mThreadInfo.getId())) {
mDAO.insertThreadInfo(mThreadInfo);
}
} catch (Exception e) {
e.printStackTrace();
}
HttpURLConnection connection = null;
RandomAccessFile raf = null;
InputStream input = null;
try {
URL url = new URL(mThreadInfo.getUrl());
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(3000);
connection.setRequestMethod("GET");
// 设置下载位置
long start = mThreadInfo.getFinished();// 上一次保存的下载进度即为这次要开始下载的地方
LogUtils.v(TAG, "上一次保存的下载进度----------->" + start);
connection.setRequestProperty("Range", "bytes=" + start + "-"
+ mThreadInfo.getEnd());
// 设置请求属性,将field参数设置为Range(范围),newValues为指定的字节数区间
// 设置文件写入位置
File file = new File(DownloadService.DOWNLOAD_PATH,
mFileInfo.getFileName() + ".apk");
raf = new RandomAccessFile(file, "rwd");
raf.seek(start);
// 在读写的时候跳过设置的字节数,从下一个字节数开始读写
// 如seek(100)则跳过100个字节从第101个字节开始读写
Intent intent = new Intent(DownloadService.ACTION_UPDATE);
mFinished = start;
LogUtils.i(TAG, "connection.getResponseCode()-------->"
+ connection.getResponseCode());
LogUtils.i(TAG, "HttpStatus.SC_PARTIAL_CONTENT-------->"
+ HttpStatus.SC_PARTIAL_CONTENT);
// 开始下载
if (connection.getResponseCode() == 200
|| connection.getResponseCode() == 206) {
// 因为前面的RequestProperty设置的Range,服务器会认为进行部分的下载,所以这里判断是否成功连接要用SC_PARTIAL_CONTENT
// 读取数据
input = connection.getInputStream();
byte[] buffer = new byte[1024 * 4];
int len = -1;// 标记每次读取的长度
long time = System.currentTimeMillis();
while ((len = input.read(buffer)) != -1) {
// 写入文件
raf.write(buffer, 0, len);
// 把下载进度发送给Activity
mFinished += len;
// 因为该循环运行较快,所以这里减缓一下UI更新的频率
if (System.currentTimeMillis() - time > 500) {
time = System.currentTimeMillis();
intent.putExtra("finished",
(int) ((mFinished * 100) / mFileInfo
.getLength()));
mContext.sendBroadcast(intent);
mDAO.updateThreadInfo(mThreadInfo.getUrl(),
mThreadInfo.getId(), mFinished);
LogUtils.d(TAG, "mFileInfo.getFileName()---"
+ mFileInfo.getFileName()
+ "---mFinished---" + mFinished + "---1");
// 如果隔断时间就更新一下数据库的内容
// 可以防止没有按下暂停就关闭程序重进后要重新开始下载的问题
// 而又不至于更新数据库太频繁,影响效率
}
// 在下载暂停时,保存下载进度至数据库
if (isPause) {
mDAO.updateThreadInfo(mThreadInfo.getUrl(),
mThreadInfo.getId(), mFinished);
intent.putExtra("finished",
(int) (mFinished * 100 / mFileInfo
.getLength()));
mContext.sendBroadcast(intent);
LogUtils.d(TAG, "mFileInfo.getFileName()---"
+ mFileInfo.getFileName()
+ "---mFinished---" + mFinished + "---2");
raf.close();
input.close();
connection.disconnect();
return;
}
}
intent.putExtra("finished",
(int) (mFinished * 100 / mFileInfo.getLength()));
mContext.sendBroadcast(intent);
// 因为有可能在刚好下载完成的时候没有进入到if(isPause)中,所以进度条会停在上次更新的时候,显示的时候还有一小段没有下载,但是实际已经下载完成了
// 删除线程信息
// mDAO.deleteThreadInfo(mThreadInfo.getUrl(),
// mThreadInfo.getId());
isFinished = true;
// 将数据提交服务器
// 在完成进行安装
//静默安装
//InstallApkTask(mThreadInfo.getApkName() + ".apk");
//普通安装
InstallApkNormal(mThreadInfo.getApkName() + ".apk");
}
} catch (Exception e) {
e.printStackTrace();
isPause = true;
} finally {
try {
raf.close();
input.close();
connection.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
/**
* 安装apk(静默)
*/
private void InstallApkTask(String fileString) {
LogUtils.e(TAG, "fileString----->" + fileString);
String fileName = DownloadService.DOWNLOAD_PATH + "/" + fileString;
String[] command = { "chmod", "777", fileName };
ProcessBuilder builder = new ProcessBuilder(command);
try {
builder.start();
} catch (IOException e) {
e.printStackTrace();
}
File f = new File(fileName);
if (!f.exists()) {
Toast.makeText(MainActivity.context, "文件不存在!", Toast.LENGTH_SHORT)
.show();
return;
}
ComponentName localComponentName = new ComponentName(
"com.vigroup.vgservice",
"com.vigroup.vgservice.InstallAPKActivity");
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setComponent(localComponentName);
intent.putExtra("apkUrl", fileName);
intent.putExtra("restartFlag", true);
intent.putExtra("packageName", "apk的包名");
MainActivity.context.startActivity(intent);
}
/**
* 普通安裝
*/
private void InstallApkNormal(String fileString) {
String fileName = DownloadService.DOWNLOAD_PATH + "/" + fileString;
String[] command = { "chmod", "777", fileName };
ProcessBuilder builder = new ProcessBuilder(command);
try {
builder.start();
} catch (IOException e) {
e.printStackTrace();
}
File f = new File(fileName);
if (f.exists()) {
installAPK(f);
} else {
Toast.makeText(MainActivity.context, "apk 不存在!", Toast.LENGTH_SHORT)
.show();
}
}
public void installAPK(File t) {
// TODO Auto-generated method stub
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(Uri.fromFile(t),
"application/vnd.android.package-archive");
mContext.startActivity(intent);
}
}
DBHelper.class:
/**
* 数据库帮助类 用来创建数据库
*
*/
public class DBHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "download.db";
private static final int VERSION = 1;// 数据库的版本
/**
* 建表语法(apk下载信息表)
*/
private static final String TABLE_CREATE = "create table thread_info(_id integer primary key autoincrement,"
+ "thread_id integer,url text,start double,end double,finished double, versionCode double,apkName varchar)";
/**
* 删表语法
*/
private static final String TABLE_DROP_THREAD= "drop table if exits thread_info";
/**
* 删表语法
*/
private static final String TABLE_DROP_OTHER = "drop table if exits other_apk";
public DBHelper(Context context) {
super(context, DB_NAME, null, VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(TABLE_CREATE);
db.execSQL(TABLE_CREATE_OTHER_APK);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL(TABLE_DROP_THREAD);
db.execSQL(TABLE_DROP_OTHER);
onCreate(db);
}
}
ThreadDAO.class:
/**
* 数据访问接口i
* @author Just
*
*/
public interface ThreadDAO {
/**
* 插入线程信息
* @param threadInfo
*/
public void insertThreadInfo(ThreadInfo threadInfo);
/**
* 删除线程信息
* @param url 文件的url
* @param id 线程的id
*/
public void deleteThreadInfo(String url,int id);
/**
* 更新线程下载进度
*/
public void updateThreadInfo(String url,int threadId,long finished);
/**
* 查询下载文件的线程信息
*/
public List<ThreadInfo> getThreads(String url);
/**
* 判断指定线程信息是否已经存在数据库中
*/
public boolean isExists(String url,int threadId);
/**
* 查询下载文件的线程信息
*/
public ThreadInfo getThread(String url);
}
ThreadDAOImpl.class:
/**
* 线程数据访问接口实现
*
*/
public class ThreadDAOImpl implements ThreadDAO {
private DBHelper mHelper;
public ThreadDAOImpl(Context context) {
mHelper = new DBHelper(context);
}
@Override
public void insertThreadInfo(ThreadInfo threadInfo) {
SQLiteDatabase db = mHelper.getWritableDatabase();
db.execSQL(
"insert into thread_info(thread_id,url,start,end,finished,versionCode,apkName) values(?,?,?,?,?,?,?)",
new Object[] { threadInfo.getId(), threadInfo.getUrl(),
threadInfo.getStart(), threadInfo.getEnd(),
threadInfo.getFinished(), threadInfo.getVersionCode(),
threadInfo.getApkName() });
db.close();
}
@Override
public void deleteThreadInfo(String url, int id) {
SQLiteDatabase db = mHelper.getWritableDatabase();
db.execSQL("delete from thread_info where url = ? and thread_id = ?",
new Object[] { url, id });
db.close();
}
@Override
public void updateThreadInfo(String url, int threadId, long finished) {
SQLiteDatabase db = mHelper.getWritableDatabase();
db.execSQL(
"update thread_info set finished = ? where url = ? and thread_id = ?",
new Object[] { finished, url, threadId });
db.close();
}
@Override
public List<ThreadInfo> getThreads(String url) {
List<ThreadInfo> list = null;
SQLiteDatabase db = mHelper.getWritableDatabase();
Cursor cursor = db.rawQuery("select * from thread_info where url = ?",
new String[] { url });
if (cursor != null) {
list = new ArrayList<ThreadInfo>();
while (cursor.moveToNext()) {
ThreadInfo temp = new ThreadInfo();
temp.setId(cursor.getInt(cursor.getColumnIndex("thread_id")));
temp.setUrl(cursor.getString(cursor.getColumnIndex("url")));
temp.setStart(cursor.getLong(cursor.getColumnIndex("start")));
temp.setEnd(cursor.getLong(cursor.getColumnIndex("end")));
temp.setFinished(cursor.getLong(cursor
.getColumnIndex("finished")));
temp.setVersionCode(cursor.getLong(cursor
.getColumnIndex("versionCode")));
temp.setApkName(cursor.getString(cursor
.getColumnIndex("apkName")));
list.add(temp);
}
cursor.close();
}
db.close();
return list;
}
@Override
public boolean isExists(String url, int threadId) {
SQLiteDatabase db = mHelper.getWritableDatabase();
Cursor cursor = db.rawQuery(
"select * from thread_info where url = ? and thread_id = ?",
new String[] { url, "" + threadId });
boolean exists = false;
if (cursor != null) {
exists = cursor.moveToNext();
}
db.close();
return exists;
}
@Override
public ThreadInfo getThread(String url) {
ThreadInfo temp = null;
SQLiteDatabase db = mHelper.getWritableDatabase();
Cursor cursor = db.rawQuery("select * from thread_info where url = ?",
new String[] { url });
if (cursor != null) {
while (cursor.moveToNext()) {
temp=new ThreadInfo();
temp.setId(cursor.getInt(cursor.getColumnIndex("thread_id")));
temp.setUrl(cursor.getString(cursor.getColumnIndex("url")));
temp.setStart(cursor.getLong(cursor.getColumnIndex("start")));
temp.setEnd(cursor.getLong(cursor.getColumnIndex("end")));
temp.setVersionCode(cursor.getLong(cursor
.getColumnIndex("versionCode")));
temp.setApkName(cursor.getString(cursor
.getColumnIndex("apkName")));
temp.setFinished(cursor.getLong(cursor
.getColumnIndex("finished")));
}
cursor.close();
}
db.close();
return temp;
}
}
以上的就是单线程断点续传的只要代码逻辑了