(Android小应用)在Android中实现多线程断点下载(连载二)

前面已经讲了通过多线程下载网络文件的一部分核心代码,下面继续做我们的项目

当然如果我们做小一点的Android项目的时候可以先做界面,然后根据需要来扩展所需要的功能,当然有的时候我们宁愿先实现业务,业务功能做好了再实现界面,因为界面里面基本不含有技术,况且我们只是为了学习,界面不需要多美观的,能用就行,OK,继续

先做数据库这层

关于建Android项目方法的截图就省略了,我用的模拟器版本是2.3.3,至今为至最新的吧

首先建一个类DBOpenHelper继续自SQLiteOpenHelper

  
  
package com.studio.service; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; public class DBOpenHelper extends SQLiteOpenHelper { private static final String DBNAME = " download.db " ; private static final int VERSION = 1 ; public DBOpenHelper(Context context) { super (context, DBNAME, null , VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL( " CREATE TABLE IF NOT EXISTS filedownlog (id integer primary key autoincrement, downpath varchar(100), threadid INTEGER, downlength INTEGER) " ); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL( " DROP TABLE IF EXISTS filedownlog " ); onCreate(db); } }

这里定义的数据名字叫download.db,版本号为1,表名filedownlog,里面有三个字段,一个id,主键,下载路径downpath,线程ID和一个下载长度downlength

然后建一个FileService类

  
  
package com.studio.service; import java.util.HashMap; import java.util.Map; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; /** * 业务bean * */ public class FileService { private DBOpenHelper openHelper; public FileService(Context context) { openHelper = new DBOpenHelper(context); } /** * 获取每条线程已经下载的文件长度 * * @param path * @return */ public Map < Integer, Integer > getData(String path) { SQLiteDatabase db = openHelper.getReadableDatabase(); Cursor cursor = db .rawQuery( " select threadid, downlength from filedownlog where downpath=? " , new String[] { path }); Map < Integer, Integer > data = new HashMap < Integer, Integer > (); while (cursor.moveToNext()) { data.put(cursor.getInt( 0 ), cursor.getInt( 1 )); } cursor.close(); db.close(); return data; } /** * 保存每条线程已经下载的文件长度 * * @param path * @param map */ public void save(String path, Map < Integer, Integer > map) { // int threadid, // int position SQLiteDatabase db = openHelper.getWritableDatabase(); db.beginTransaction(); try { for (Map.Entry < Integer, Integer > entry : map.entrySet()) { db.execSQL( " insert into filedownlog(downpath, threadid, downlength) values(?,?,?) " , new Object[] { path, entry.getKey(), entry.getValue() }); } db.setTransactionSuccessful(); } finally { db.endTransaction(); } db.close(); } /** * 实时更新每条线程已经下载的文件长度 * * @param path * @param map */ public void update(String path, Map < Integer, Integer > map) { SQLiteDatabase db = openHelper.getWritableDatabase(); db.beginTransaction(); try { for (Map.Entry < Integer, Integer > entry : map.entrySet()) { db.execSQL( " update filedownlog set downlength=? where downpath=? and threadid=? " , new Object[] { entry.getValue(), path, entry.getKey() }); } db.setTransactionSuccessful(); } finally { db.endTransaction(); } db.close(); } /** * 当文件下载完成后,删除对应的下载记录 * * @param path */ public void delete(String path) { SQLiteDatabase db = openHelper.getWritableDatabase(); db.execSQL( " delete from filedownlog where downpath=? " , new Object[] { path }); db.close(); } }

类中都有相应的注释的,看不懂可以慢慢看……

因为要用到进度条,所以先把进度条建了

  
  
package com.studio.net.download; public interface DownloadProgressListener { public void onDownloadSize( int size); }
这里我们定义了一个进度条监听接口,由实现此接口的类来调用onDownloadSize()方法

新建一个类DownloadThread继承自Thread

  
  
package com.studio.net.download; import java.io.File; import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.URL; import android.util.Log; public class DownloadThread extends Thread { private static final String TAG = " DownloadThread " ; private File saveFile; private URL downUrl; private int block; /* 下载开始位置 */ private int threadId = - 1 ; private int downLength; private boolean finish = false ; private FileDownloader downloader; public DownloadThread(FileDownloader downloader, URL downUrl, File saveFile, int block, int downLength, int threadId) { this .downUrl = downUrl; this .saveFile = saveFile; this .block = block; this .downloader = downloader; this .threadId = threadId; this .downLength = downLength; } @Override public void run() { if (downLength < block) { // 未下载完成 try { HttpURLConnection http = (HttpURLConnection) downUrl .openConnection(); http.setConnectTimeout( 5 * 1000 ); http.setRequestMethod( " GET " ); http.setRequestProperty( " Accept " , " image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */* " ); http.setRequestProperty( " Accept-Language " , " zh-CN " ); http.setRequestProperty( " Referer " , downUrl.toString()); http.setRequestProperty( " Charset " , " UTF-8 " ); int startPos = block * (threadId - 1 ) + downLength; // 开始位置 int endPos = block * threadId - 1 ; // 结束位置 http.setRequestProperty( " Range " , " bytes= " + startPos + " - " + endPos); // 设置获取实体数据的范围 http.setRequestProperty( " User-Agent " , " Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729) " ); http.setRequestProperty( " Connection " , " Keep-Alive " ); InputStream inStream = http.getInputStream(); byte [] buffer = new byte [ 1024 ]; int offset = 0 ; print( " Thread " + this .threadId + " start download from position " + startPos); RandomAccessFile threadfile = new RandomAccessFile( this .saveFile, " rwd " ); threadfile.seek(startPos); while ((offset = inStream.read(buffer, 0 , 1024 )) != - 1 ) { threadfile.write(buffer, 0 , offset); downLength += offset; downloader.update( this .threadId, downLength); downloader.append(offset); } threadfile.close(); inStream.close(); print( " Thread " + this .threadId + " download finish " ); this .finish = true ; } catch (Exception e) { this .downLength = - 1 ; print( " Thread " + this .threadId + " : " + e); } } } private static void print(String msg) { Log.i(TAG, msg); } /** * 下载是否完成 * * @return */ public boolean isFinish() { return finish; } /** * 已经下载的内容大小 * * @return 如果返回值为-1,代表下载失败 */ public long getDownLength() { return downLength; } }

再建一个文件下载器FileDownloader类

  
  
package com.studio.net.download; import java.io.File; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.URL; import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.studio.service.FileService; import android.content.Context; import android.util.Log; public class FileDownloader { private static final String TAG = " FileDownloader " ; private Context context; private FileService fileService; /* 已下载文件长度 */ private int downloadSize = 0 ; /* 原始文件长度 */ private int fileSize = 0 ; /* 线程数 */ private DownloadThread[] threads; /* 本地保存文件 */ private File saveFile; /* 缓存各线程下载的长度 */ private Map < Integer, Integer > data = new ConcurrentHashMap < Integer, Integer > (); /* 每条线程下载的长度 */ private int block; /* 下载路径 */ private String downloadUrl; /** * 获取线程数 */ public int getThreadSize() { return threads.length; } /** * 获取文件大小 * * @return */ public int getFileSize() { return fileSize; } /** * 累计已下载大小 * * @param size */ protected synchronized void append( int size) { downloadSize += size; } /** * 更新指定线程最后下载的位置 * * @param threadId * 线程id * @param pos * 最后下载的位置 */ protected synchronized void update( int threadId, int pos) { this .data.put(threadId, pos); this .fileService.update( this .downloadUrl, this .data); } /** * 构建文件下载器 * * @param downloadUrl * 下载路径 * @param fileSaveDir * 文件保存目录 * @param threadNum * 下载线程数 */ public FileDownloader(Context context, String downloadUrl, File fileSaveDir, int threadNum) { try { this .context = context; this .downloadUrl = downloadUrl; fileService = new FileService( this .context); URL url = new URL( this .downloadUrl); if ( ! fileSaveDir.exists()) fileSaveDir.mkdirs(); this .threads = new DownloadThread[threadNum]; HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout( 5 * 1000 ); conn.setRequestMethod( " GET " ); conn.setRequestProperty( " Accept " , " image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */* " ); conn.setRequestProperty( " Accept-Language " , " zh-CN " ); conn.setRequestProperty( " Referer " , downloadUrl); conn.setRequestProperty( " Charset " , " UTF-8 " ); conn.setRequestProperty( " User-Agent " , " Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729) " ); conn.setRequestProperty( " Connection " , " Keep-Alive " ); conn.connect(); printResponseHeader(conn); if (conn.getResponseCode() == 200 ) { this .fileSize = conn.getContentLength(); // 根据响应获取文件大小 if ( this .fileSize <= 0 ) throw new RuntimeException( " Unkown file size " ); String filename = getFileName(conn); // 获取文件名称 this .saveFile = new File(fileSaveDir, filename); // 构建保存文件 Map < Integer, Integer > logdata = fileService .getData(downloadUrl); // 获取下载记录 if (logdata.size() > 0 ) { // 如果存在下载记录 for (Map.Entry < Integer, Integer > entry : logdata.entrySet()) data.put(entry.getKey(), entry.getValue()); // 把各条线程已经下载的数据长度放入data中 } if ( this .data.size() == this .threads.length) { // 下面计算所有线程已经下载的数据长度 for ( int i = 0 ; i < this .threads.length; i ++ ) { this .downloadSize += this .data.get(i + 1 ); } print( " 已经下载的长度 " + this .downloadSize); } // 计算每条线程下载的数据长度 this .block = ( this .fileSize % this .threads.length) == 0 ? this .fileSize / this .threads.length : this .fileSize / this .threads.length + 1 ; } else { throw new RuntimeException( " server no response " ); } } catch (Exception e) { print(e.toString()); throw new RuntimeException( " don't connection this url " ); } } /** * 获取文件名 */ private String getFileName(HttpURLConnection conn) { String filename = this .downloadUrl.substring( this .downloadUrl .lastIndexOf( ' / ' ) + 1 ); if (filename == null || "" .equals(filename.trim())) { // 如果获取不到文件名称 for ( int i = 0 ;; i ++ ) { String mine = conn.getHeaderField(i); if (mine == null ) break ; if ( " content-disposition " .equals(conn.getHeaderFieldKey(i) .toLowerCase())) { Matcher m = Pattern.compile( " .*filename=(.*) " ).matcher( mine.toLowerCase()); if (m.find()) return m.group( 1 ); } } filename = UUID.randomUUID() + " .tmp " ; // 默认取一个文件名 } return filename; } /** * 开始下载文件 * * @param listener * 监听下载数量的变化,如果不需要了解实时下载的数量,可以设置为null * @return 已下载文件大小 * @throws Exception */ public int download(DownloadProgressListener listener) throws Exception { try { RandomAccessFile randOut = new RandomAccessFile( this .saveFile, " rw " ); if ( this .fileSize > 0 ) randOut.setLength( this .fileSize); randOut.close(); URL url = new URL( this .downloadUrl); if ( this .data.size() != this .threads.length) { this .data.clear(); for ( int i = 0 ; i < this .threads.length; i ++ ) { this .data.put(i + 1 , 0 ); // 初始化每条线程已经下载的数据长度为0 } } for ( int i = 0 ; i < this .threads.length; i ++ ) { // 开启线程进行下载 int downLength = this .data.get(i + 1 ); if (downLength < this .block && this .downloadSize < this .fileSize) { // 判断线程是否已经完成下载,否则继续下载 this .threads[i] = new DownloadThread( this , url, this .saveFile, this .block, this .data.get(i + 1 ), i + 1 ); this .threads[i].setPriority( 7 ); this .threads[i].start(); } else { this .threads[i] = null ; } } this .fileService.save( this .downloadUrl, this .data); boolean notFinish = true ; // 下载未完成 while (notFinish) { // 循环判断所有线程是否完成下载 Thread.sleep( 900 ); notFinish = false ; // 假定全部线程下载完成 for ( int i = 0 ; i < this .threads.length; i ++ ) { if ( this .threads[i] != null && ! this .threads[i].isFinish()) { // 如果发现线程未完成下载 notFinish = true ; // 设置标志为下载没有完成 if ( this .threads[i].getDownLength() == - 1 ) { // 如果下载失败,再重新下载 this .threads[i] = new DownloadThread( this , url, this .saveFile, this .block, this .data.get(i + 1 ), i + 1 ); this .threads[i].setPriority( 7 ); this .threads[i].start(); } } } if (listener != null ) listener.onDownloadSize( this .downloadSize); // 通知目前已经下载完成的数据长度 } fileService.delete( this .downloadUrl); } catch (Exception e) { print(e.toString()); throw new Exception( " file download fail " ); } return this .downloadSize; } /** * 获取Http响应头字段 * * @param http * @return */ public static Map < String, String > getHttpResponseHeader( HttpURLConnection http) { Map < String, String > header = new LinkedHashMap < String, String > (); for ( int i = 0 ;; i ++ ) { String mine = http.getHeaderField(i); if (mine == null ) break ; header.put(http.getHeaderFieldKey(i), mine); } return header; } /** * 打印Http头字段 * * @param http */ public static void printResponseHeader(HttpURLConnection http) { Map < String, String > header = getHttpResponseHeader(http); for (Map.Entry < String, String > entry : header.entrySet()) { String key = entry.getKey() != null ? entry.getKey() + " : " : "" ; print(key + entry.getValue()); } } private static void print(String msg) { Log.i(TAG, msg); } }

OK至此业务写完了

现在来设计界面UI,由于要使用到String类型的字符串,所以我们先在String.xml中定义一下

添加

  
  
< string name ="path" > 下载路径 </ string > < string name ="button" > 下载 </ string > < string name ="suceess" > 下载完成 </ string > < string name ="error" > 下载失败 </ string > < string name ="sdcarderror" > SDCard不存在或写保护 </ string >

界面main.xml

  
  
<? xml version="1.0" encoding="utf-8" ?> < LinearLayout xmlns:android ="http://schemas.android.com/apk/res/android" android:orientation ="vertical" android:layout_width ="fill_parent" android:layout_height ="fill_parent" > < TextView android:layout_width ="fill_parent" android:layout_height ="wrap_content" android:text ="@string/path" /> < EditText android:layout_width ="fill_parent" android:layout_height ="wrap_content" android:text ="http://www.mp3upload.ca/upload/42981-fdt.mp3" android:id ="@+id/path" /> < Button android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:text ="@string/button" android:id ="@+id/button" /> < ProgressBar android:layout_width ="fill_parent" android:layout_height ="20px" style ="?android:attr/progressBarStyleHorizontal" android:id ="@+id/downloadbar" /> < TextView android:layout_width ="fill_parent" android:layout_height ="wrap_content" android:gravity ="center" android:id ="@+id/result" /> </ LinearLayout >

主类MainActivity

  
  
package com.studio.download; import java.io.File; import com.studio.net.download.DownloadProgressListener; import com.studio.net.download.FileDownloader; import android.app.Activity; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.Message; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; public class MainActivity extends Activity { private EditText pathText; private ProgressBar downloadbar; private TextView resultView; // 它用于往消息队列发送消息,当Handler被创建时会自动绑定到Handler被创建时所在的线程所绑定的消息队列 private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case 1 : downloadbar.setProgress(msg.getData().getInt( " size " )); // 把当前已经下载的数据长度设置为进度条的当前刻度 float num = ( float ) downloadbar.getProgress() / ( float ) downloadbar.getMax(); int result = ( int ) (num * 100 ); resultView.setText(result + " % " ); if (downloadbar.getProgress() == downloadbar.getMax()) { Toast.makeText(MainActivity. this , R.string.suceess, 1 ) .show(); } break ; case - 1 : Toast.makeText(MainActivity. this , R.string.error, 1 ).show(); break ; } } }; @Override public void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.main); pathText = (EditText) findViewById(R.id.path); downloadbar = (ProgressBar) findViewById(R.id.downloadbar); resultView = (TextView) findViewById(R.id.result); Button button = (Button) findViewById(R.id.button); button.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { if (Environment.getExternalStorageState().equals( Environment.MEDIA_MOUNTED)) { String path = pathText.getText().toString(); download(path, Environment.getExternalStorageDirectory()); } else { Toast.makeText(MainActivity. this , R.string.sdcarderror, 1 ) .show(); } } }); } // 对UI控件的更新只能由主线程(UI线程)负责,如果不在UI线程更新控件,更新后的值不会被重绘到屏幕上 private void download( final String path, final File saveDir) { new Thread( new Runnable() { @Override public void run() { try { FileDownloader loader = new FileDownloader( MainActivity. this , path, saveDir, 3 ); downloadbar.setMax(loader.getFileSize()); // 设置进度条的最大刻度为文件的大小 loader.download( new DownloadProgressListener() { @Override public void onDownloadSize( int size) { Message msg = new Message(); msg.what = 1 ; msg.getData().putInt( " size " , size); handler.sendMessage(msg); // msg.target = handler; } }); } catch (Exception e) { Message msg = new Message(); msg.what = - 1 ; handler.sendMessage(msg); } } }).start(); } }

当然别忘了添加权限

  
  
< uses-permission android:name ="android.permission.INTERNET" ></ uses-permission > < uses-permission android:name ="android.permission.WRITE_EXTERNAL_STORAGE" ></ uses-permission > < uses-permission android:name ="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" ></ uses-permission >
OK,搞定,运行一下

如图所示

由于这个mp3有点大,耐心等待

OK,终于下载完成了,看看sdcard吧,上面有我们刚下载的歌曲文件了,播放一下试试,呵呵

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值