转载请注明出处:http://blog.csdn.net/l1028386804/article/details/46897641

一、概述

在上一篇博文《Android之——多线程下载示例》中,我们讲解了如何实现Android的多线程下载功能,通过将整个文件分成多个数据块,开启多个线程,让每个线程分别下载一个相应的数据块来实现多线程下载的功能。多线程下载中,可以将下载这个耗时的操作放在子线程中执行,即不阻塞主线程,又符合Android开发的设计规范。

但是当下载的过程当中突然出现手机卡死,或者网络中断,手机电量不足关机的现象,这时,当手机可以正常使用后,如果重新下载文件,似乎不太符合大多数用户的心理期望,那如何实现当手机可以正常联网时,基于上次断网时下载的数据来下载呢?这就是所谓的断点下载了。这篇文章主要是讲解如何实现断点下载的功能。

本文讲解的Android断点下载是基于上一篇文章《Android之——多线程下载示例》,本示例是在上一示例的基础上通过在下载的过程中,将下载的信息保存到Andoid系统自带的数据库SQLite中,当手机出现异常情况而断开网络时,由于数据库中记录了上次下载的数据信息,当手机再次联网时,读取数据库中的信息,从上次断开下载的地方继续下载数据。好,不多说了,进入正文。

二、服务端准备

服务端的实现很简单,这里为了使下载的文件大些,我在网络上下载了有道词典来作为要下载的测试资源。将它放置在项目的WebContent目录下,并将项目发布在Tomcat服务器中,具体如下图所示:

就这样,服务端算是弄好了,怎么样?很简单吧?相信大家都会的!

三、Android实现

Android实现部分是本文的重点,这里我们从布局开始由浅入深慢慢讲解,这里我们通过Activity来显示程序的界面,以SQLite数据库来保存下载的信息,通过ContentProvider来操作保存的记录信息,通过Handler和Message机制将子线程中的数据传递到主线程来更新UI显示。同时通过自定义监听器来实现对UI显示更新的监听操作。

1、布局实现

布局基本上和上一博文中的布局一样,没有什么大的变动,界面上自上而下放置一个TextView,用来提示文本框中输入的信息,一个文本框用来输入网络中下载文件的路径,一个Button按钮,点击下载文件,一个ProgressBar显示下载进度,一个TextView显示下载的百分比。

具体布局内容如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:paddingBottom="@dimen/activity_vertical_margin"    android:paddingLeft="@dimen/activity_horizontal_margin"    android:paddingRight="@dimen/activity_horizontal_margin"    android:paddingTop="@dimen/activity_vertical_margin"    android:orientation="vertical"    tools:context=".MainActivity" >    <TextView        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="下载路径" />        <EditText         android:id="@+id/ed_path"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="http://192.168.0.170:8080/web/youdao.exe"/>    <Button         android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="下载"        android:onClick="download"/>        <ProgressBar         android:id="@+id/pb"        android:layout_width="match_parent"        android:layout_height="wrap_content"        style="@android:style/Widget.ProgressBar.Horizontal"/>        <TextView         android:id="@+id/tv_info"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:gravity="center"        android:text="下载:0%"/></LinearLayout>

2、自定义ProgressBarListener监听器接口

新建自定义ProgressBarListener监听器接口,这个接口中定义两个方法,void getMax(int length)用来获取下载文件的长度,void getDownload(int length);用来获取每次下载的长度,这个方法中主要是在多线程中调用,子线程中获取到的数据传递到这两个接口方法中,然后在这两个接口方法中通过Handler将相应的长度信息传递到主线程,更新界面显示信息。

具体代码实现如下:

package com.example.inter;/** * 自定义进度条监听器 * @author liuyazhuang * */public interface ProgressBarListener {/** * 获取文件的长度 * @param length */void getMax(int length);/** * 获取每次下载的长度 * @param length */void getDownload(int length);}

3.定义数据库的相关信息类DownloadDBHelper

在这个实例中,我们将数据库的名称定义为download.db,我们需要保存主键id,文件下载后要保存的路径,每个线程的标识id,每个线程下载的文件数据块大小,所以,在创建的数据表中共有_id, path,threadid,downloadlength,详情见下图

DownloadDBHelper实现的具体代码如下:

package com.example.db;import android.content.Context;import android.database.sqlite.SQLiteDatabase;import android.database.sqlite.SQLiteDatabase.CursorFactory;import android.database.sqlite.SQLiteOpenHelper;/** * 数据库相关类 * @author liuyazhuang * */public class DownloadDBHelper extends SQLiteOpenHelper {/** * 数据库名称 */private static final String NAME = "download.db";/** * 原有的构造方法 * @param context * @param name * @param factory * @param version */public DownloadDBHelper(Context context, String name,CursorFactory factory, int version) {super(context, name, factory, version);}/** * 重载构造方法 * @param context */public DownloadDBHelper(Context context){super(context, NAME, null, 1);}/**  * 创建数据库时调用 */@Overridepublic void onCreate(SQLiteDatabase db) { db.execSQL("create table download(_id integer primary key autoincrement," +        "path text," +        "threadid integer," +        "downloadlength integer)");}/**  * 更新数据库时调用 */@Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {}}

4、创建DownloadProvider类

DownloadProvider类继承自ContentProvider,提供操作数据库的方法,在这个类中,通过UriMatcher类匹配要操作的数据库,通过DownloadDBHelper对象来得到一个具体数据库实例,来对相应的数据库进行增、删、改、查操作。

具体实现如下代码所示:

package com.example.provider;import com.example.db.DownloadDBHelper;import android.content.ContentProvider;import android.content.ContentValues;import android.content.UriMatcher;import android.database.Cursor;import android.database.sqlite.SQLiteDatabase;import android.database.sqlite.SQLiteOpenHelper;import android.net.Uri;/** * 自定义ContentProvider实例 * @author liuyazhuang * */public class DownloadProvider extends ContentProvider {//实例化UriMatcher对象private static UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);//配置访问规则private static final String AUTHORITY = "download";//自定义常量private static final int DOWANLOAD = 10;static{//添加匹配的规则matcher.addURI(AUTHORITY, "download", DOWANLOAD);}private SQLiteOpenHelper mOpenHelper;@Overridepublic boolean onCreate() {mOpenHelper = new DownloadDBHelper(getContext());return false;}@Overridepublic Cursor query(Uri uri, String[] projection, String selection,String[] selectionArgs, String sortOrder) {// TODO Auto-generated method stubCursor ret = null;SQLiteDatabase db = mOpenHelper.getReadableDatabase();int code = matcher.match(uri);switch (code) {case DOWANLOAD:ret = db.query("download", projection, selection, selectionArgs, null, null, sortOrder);break;default:break;}return ret;}@Overridepublic String getType(Uri uri) {// TODO Auto-generated method stubreturn null;}@Overridepublic Uri insert(Uri uri, ContentValues values) {// TODO Auto-generated method stubSQLiteDatabase db = mOpenHelper.getWritableDatabase();int code = matcher.match(uri);switch (code) {case DOWANLOAD:db.insert("download", "_id", values);break;default:break;}return null;}@Overridepublic int delete(Uri uri, String selection, String[] selectionArgs) {SQLiteDatabase db = mOpenHelper.getWritableDatabase();int code = matcher.match(uri);switch (code) {case DOWANLOAD:db.delete("download", selection, selectionArgs);break;default:break;}return 0;}@Overridepublic int update(Uri uri, ContentValues values, String selection,String[] selectionArgs) {SQLiteDatabase db = mOpenHelper.getWritableDatabase();int code = matcher.match(uri);switch (code) {case DOWANLOAD:db.update("download", values, selection, selectionArgs);break;default:break;}return 0;}}

5、创建DownloadInfo实体类

为了使程序更加面向对象化,这里我们建立DownloadInfo实体类来对数据库中的数据进行封装,DownloadInfo实体类中的数据字段与数据库中的字段相对应

具体实现代码如下:

package com.example.domain;/** * 支持断点续传时, * 要保存到数据库的信息 * @author liuyazhuang * */public class DownloadInfo {//主键idprivate int _id;//保存路径private String path;//线程的标识idprivate String threadId;//下载文件的大小private int downloadSize;public DownloadInfo() {super();}public DownloadInfo(int _id, String path, String threadId, int downloadSize) {super();this._id = _id;this.path = path;this.threadId = threadId;this.downloadSize = downloadSize;}public int get_id() {return _id;}public void set_id(int _id) {this._id = _id;}public String getPath() {return path;}public void setPath(String path) {this.path = path;}public String getThreadId() {return threadId;}public void setThreadId(String threadId) {this.threadId = threadId;}public int getDownloadSize() {return downloadSize;}public void setDownloadSize(int downloadSize) {this.downloadSize = downloadSize;}}

6、定义外界调用的操作数据库的方法类DownloadDao

DownloadDao类中封装了一系列操作数据库的方法,这个类不是直接操作数据库对象,而是通过ContentResolver这个对象来调用DownloadProvider中的方法来实现操作数据库的功能,这里用到了ContentResolver与ContentProvider这两个Android中非常重要的类。ContentProvider即内容提供者,主要是向外提供数据,简单理解就是一个应用程序可以通过ContentProvider向外提供操作本应用程序的接口,其他应用程序可以调用ContentProvider提供的接口来操作本应用程序的数据。ContentResolver内容接接收者,它可以接收ContentProvider的向外提供的数据。

具体代码实现如下:

package com.example.dao;import android.content.ContentResolver;import android.content.ContentValues;import android.content.Context;import android.database.Cursor;import android.net.Uri;import com.example.domain.DownloadInfo;/** * 保存下载文件信息的dao类 * @author liuyazhuang * */public class DownloadDao {/** * ContentResolver对象 */private ContentResolver cr;public DownloadDao(Context context){this.cr = context.getContentResolver();}/** * 保存下载信息记录 * @param info */public void save(DownloadInfo info){Uri uri = Uri.parse("content://download/download");ContentValues values = new ContentValues();values.put("path", info.getPath());values.put("threadid", info.getThreadId());cr.insert(uri, values);}/** * 更新下载信息记录 * @param info */public void update(DownloadInfo info){Uri uri = Uri.parse("content://download/download");ContentValues values = new ContentValues();values.put("downloadlength", info.getDownloadSize());values.put("threadid", info.getThreadId());cr.update(uri, values, " path = ? and threadid = ? ", new String[]{info.getPath(), info.getThreadId()});}/** * 删除下载信息记录 * @param info */public void delete(DownloadInfo info){Uri uri = Uri.parse("content://download/download");cr.delete(uri, " path = ? and threadid = ? ", new String[]{info.getPath(), info.getThreadId()});}/** * 删除下载信息记录 * @param info */public void delete(String path){Uri uri = Uri.parse("content://download/download");cr.delete(uri, " path = ? ", new String[]{path});}/** * 判断是否有下载记录 * @param path * @return */public boolean isExist(String path){boolean result = false;Uri uri = Uri.parse("content://download/download");Cursor cursor = cr.query(uri, null, " path = ? ", new String[]{path}, null);if(cursor.moveToNext()){result = true;}cursor.close();return result;}/** * 计算所有的下载长度 * @param path * @return */public int queryCount(String path){int count = 0;Uri uri = Uri.parse("content://download/download");Cursor cursor = cr.query(uri, new String[]{"downloadlength"}, " path = ? ", new String[]{path}, null);while(cursor.moveToNext()){int len = cursor.getInt(0);count += len;}cursor.close();return count;}/** * 计算每个线程的下载长度 * @param path * @return */public int query(DownloadInfo info){int count = 0;Uri uri = Uri.parse("content://download/download");Cursor cursor = cr.query(uri, new String[]{"downloadlength"}, " path = ? and threadid = ?", new String[]{info.getPath(), info.getThreadId()}, null);while(cursor.moveToNext()){int len = cursor.getInt(0);count += len;}cursor.close();return count;}}

7、自定义线程类DownThread

这里通过继承Thread的方式来实现自定义线程操作,在这个类中主要是实现文件的下载操作,在这个类中,定义了一系列与下载有关的实例变量来控制下载的数据,通过自定义监听器ProgressBarListener中的void getDownload(int length)方法来跟新界面显示的进度信息,同时通过调用DownloadDao的方法来记录和更新数据的下载信息。

具体实现代码如下:

package com.example.download;import java.io.File;import java.io.InputStream;import java.io.RandomAccessFile;import java.net.HttpURLConnection;import java.net.URL;import android.content.Context;import com.example.dao.DownloadDao;import com.example.domain.DownloadInfo;import com.example.inter.ProgressBarListener;/** * 自定义线程类 * @author liuyazhuang * */public class DownloadThread extends Thread {//下载的线程idprivate int threadId;//下载的文件路径private String path;//保存的文件private File file;//下载的进度条更新的监听器private ProgressBarListener listener;//每条线程下载的数据量private int block;//下载的开始位置private int startPosition;//下载的结束位置private int endPosition;private DownloadDao downloadDao;public DownloadThread(int threadId, String path, File file, ProgressBarListener listener, int block, Context context) {this.threadId = threadId;this.path = path;this.file = file;this.listener = listener;this.block = block;this.downloadDao = new DownloadDao(context);this.startPosition = threadId * block;this.endPosition = (threadId + 1) * block - 1;}@Overridepublic void run() {super.run();try {//判断该线程是否有下载记录DownloadInfo info = new DownloadInfo();info.setPath(path);info.setThreadId(String.valueOf(threadId));int length =  downloadDao.query(info);startPosition += length;//创建RandomAccessFile对象RandomAccessFile accessFile = new RandomAccessFile(file, "rwd");//跳转到开始位置accessFile.seek(startPosition);URL url = new URL(path);//打开http链接HttpURLConnection conn  = (HttpURLConnection) url.openConnection();//设置超时时间conn.setConnectTimeout(5000);//指定请求方式为GET方式conn.setRequestMethod("GET");//指定下载的位置conn.setRequestProperty("Range", "bytes="+startPosition + "-" + endPosition);//不用再去判断状态码是否为200InputStream in = conn.getInputStream();byte[] buffer = new byte[1024];int len = 0;//该线程下载的总数据量int count = length;while((len = in.read(buffer)) != -1){accessFile.write(buffer, 0, len);//更新下载进度listener.getDownload(len);count += len;info.setDownloadSize(count);//更新下载的信息downloadDao.update(info);}accessFile.close();in.close();} catch (Exception e) {// TODO: handle exceptione.printStackTrace();}}}

8、新建下载的管理类DownloadManager

这个类主要是对下载过程的管理,包括下载设置下载后文件要保存的位置,计算多线程中每个线程的数据下载量等等,同时相比《Android之——多线程下载示例》一文中,它多了多下载数据的记录与更新操作。

具体实现代码如下:

package com.example.download;import java.io.File;import java.io.RandomAccessFile;import java.net.HttpURLConnection;import java.net.URL;import android.content.Context;import android.os.Environment;import com.example.dao.DownloadDao;import com.example.domain.DownloadInfo;import com.example.inter.ProgressBarListener;/** * 文件下载管理器 * @author liuyazhuang * */public class DownloadManager {//下载线程的数量private static final int TREAD_SIZE = 3;private File file;private DownloadDao downloadDao;private Context context;public DownloadManager(Context context) {this.context = context;this.downloadDao = new DownloadDao(context);}/** * 下载文件的方法 * @param path:下载文件的路径 * @param listener:自定义的下载文件监听接口 * @throws Exception */public void download(String path, ProgressBarListener listener) throws Exception{URL url = new URL(path);HttpURLConnection conn = (HttpURLConnection) url.openConnection();conn.setConnectTimeout(5000);conn.setRequestMethod("GET");if(conn.getResponseCode() == 200){int filesize = conn.getContentLength();//设置进度条的最大长度listener.getMax(filesize);//判断下载记录是否存在boolean ret = downloadDao.isExist(path);if(ret){//得到下载的总长度,设置进度条的刻度int count = downloadDao.queryCount(path);listener.getDownload(count);}else{//保存下载记录for(int i = 0; i < filesize; i++){DownloadInfo info = new DownloadInfo();info.setPath(path);info.setThreadId(String.valueOf(i));//保存下载的记录信息downloadDao.save(info);}}//创建一个和服务器大小一样的文件file = new File(Environment.getExternalStorageDirectory(), this.getFileName(path));RandomAccessFile accessFile = new RandomAccessFile(file, "rwd");accessFile.setLength(filesize);//要关闭RandomAccessFile对象accessFile.close();//计算出每条线程下载的数据量int block = filesize % TREAD_SIZE == 0 ? (filesize / TREAD_SIZE) : (filesize / TREAD_SIZE +1 ); //开启线程下载for(int i = 0; i < TREAD_SIZE; i++){new DownloadThread(i, path, file, listener, block, context).start();}}}/** * 截取路径中的文件名称 * @param path:要截取文件名称的路径 * @return:截取到的文件名称 */private String getFileName(String path){return path.substring(path.lastIndexOf("/") + 1);}}

9、完善MainActivity

在这个类中首先,找到页面中的各个控件,实现Button按钮的onClick事件,在onClick事件中开启一个线程进行下载操作,同时子线程中获取到的数据,通过handler与Message机制传递到主线程,更新界面显示,利用DownloadDao类中的方法来记录和更新下载数据。

具体实现代码如下:

package com.example.multi;import android.app.Activity;import android.os.Bundle;import android.os.Handler;import android.os.Message;import android.view.Menu;import android.view.View;import android.widget.EditText;import android.widget.ProgressBar;import android.widget.TextView;import android.widget.Toast;import com.example.dao.DownloadDao;import com.example.download.DownloadManager;import com.example.inter.ProgressBarListener;/** * MainActivity整个应用程序的入口 * @author liuyazhuang * */public class MainActivity extends Activity {protected static final int ERROR_DOWNLOAD = 0;protected static final int SET_PROGRESS_MAX = 1;protected static final int UPDATE_PROGRESS = 2;private EditText ed_path;private ProgressBar pb;private TextView tv_info;private DownloadManager manager;private DownloadDao downloadDao;//handler操作private Handler mHandler = new Handler(){public void handleMessage(android.os.Message msg) {switch (msg.what) {case ERROR_DOWNLOAD://提示用户下载失败Toast.makeText(MainActivity.this, "下载失败", Toast.LENGTH_SHORT).show();break;case SET_PROGRESS_MAX://得到最大值int max = (Integer) msg.obj;//设置进度条的最大值pb.setMax(max);break;case UPDATE_PROGRESS://获取当前下载的长度int currentprogress = pb.getProgress();//获取新下载的长度int len = (Integer) msg.obj;//计算当前总下载长度int crrrentTotalProgress = currentprogress + len;pb.setProgress(crrrentTotalProgress);//获取总大小int maxProgress = pb.getMax();//计算百分比float value = (float)currentprogress / (float)maxProgress;int percent = (int) (value * 100);//显示下载的百分比tv_info.setText("下载:"+percent+"%");if(maxProgress == crrrentTotalProgress){//删除下载记录downloadDao.delete(ed_path.getText().toString());}break;default:break;}};};@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);this.ed_path = (EditText) super.findViewById(R.id.ed_path);this.pb = (ProgressBar) super.findViewById(R.id.pb);this.tv_info = (TextView) super.findViewById(R.id.tv_info);this.manager = new DownloadManager(this);this.downloadDao = new DownloadDao(this);}@Overridepublic boolean onCreateOptionsMenu(Menu menu) {// Inflate the menu; this adds items to the action bar if it is present.getMenuInflater().inflate(R.menu.main, menu);return true;}public void download(View v){final String path = ed_path.getText().toString();//下载new Thread(new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stubtry {manager.download(path, new ProgressBarListener() {@Overridepublic void getMax(int length) {// TODO Auto-generated method stubMessage message = new Message();message.what = SET_PROGRESS_MAX;message.obj = length;mHandler.sendMessage(message);}@Overridepublic void getDownload(int length) {// TODO Auto-generated method stubMessage message = new Message();message.what = UPDATE_PROGRESS;message.obj = length;mHandler.sendMessage(message);}});} catch (Exception e) {// TODO: handle exceptione.printStackTrace();Message message = new Message();message.what = ERROR_DOWNLOAD;mHandler.sendMessage(message);}}}).start();}}

10、增加权限

最后,别忘了给应用授权,这里要用到Android联网授权和向SD卡中写入文件的权限。

具体实现如下:

<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"    package="com.example.multi"    android:versionCode="1"    android:versionName="1.0" >    <uses-sdk        android:minSdkVersion="8"        android:targetSdkVersion="18" /><uses-permission android:name="android.permission.INTERNET"/><uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>    <application        android:allowBackup="true"        android:icon="@drawable/ic_launcher"        android:label="@string/app_name"        android:theme="@style/AppTheme" >        <activity            android:name="com.example.multi.MainActivity"            android:label="@string/app_name" >            <intent-filter>                <action android:name="android.intent.action.MAIN" />                <category android:name="android.intent.category.LAUNCHER" />            </intent-filter>        </activity>        <provider android:name="com.example.provider.DownloadProvider" android:authorities="download"></provider>    </application></manifest>

四、运行效果

此时,关闭模拟器,再次打开点击下载后

如上:实现了Android中的断点下载功能。

提醒:大家可以到http://download.csdn.net/detail/l1028386804/8903201链接来获取完整的Android断点下载示例源码

更多相关文章

  1. android中Invalidate和postInvalidate的区别
  2. android平台下基于ffmpeg的swscale模块实现对YUV和RGB数据进行转
  3. 掌握Android中的进程和线程
  4. Android(安卓)JNI 使用的数据结构JNINativeMethod详解 ||建立And
  5. Android之SQlite数据库
  6. android的UI操作单线程模型理解
  7. android-Handler源码解析
  8. Android(安卓)基础面试题目
  9. mybatisplus的坑 insert标签insert into select无参数问题的解决

随机推荐

  1. Android开发学习 之 五、基本界面控件
  2. 记Android(安卓)Studio自定义属性访问不
  3. 一些非常实用的 Android(安卓)开发资源
  4. Android实现微信和QQ“在其他应用打开”
  5. 【源代码】基于Android和蓝牙的单片机温
  6. Android学习笔记2:Hello World程序解析
  7. Android(安卓)studio 3.0安装配置方法图
  8. Android(安卓)Studio下载及安装3.0版本
  9. ViewGroup 的事件分发核心
  10. Android(安卓)实现微信,QQ的程序前后台切