Android多线程断点下载完整示例详解
MainActivity如下:
package cc.activity;import java.io.File;import android.app.Activity;import android.content.Context;import android.os.Bundle;import android.os.Environment;import android.os.Handler;import android.os.Message;import android.view.View;import android.view.View.OnClickListener;import android.widget.Button;import android.widget.EditText;import android.widget.ProgressBar;import android.widget.TextView;import android.widget.Toast;import cc.download.DownloadProgressListener;import cc.download.FileDownloader;import cn.com.downloadActivity.R;/** * Demo描述: * 多线程断点下载文件的实现 * * 思路梳理: * 1 将待下载文件切分成几部分,每部分开启一条线程进行下载 * 2 将每条线程下载的部分利用RandomAccessFile写入本地文件 * 3 在下载过程中不断将该线程已经下载的数据位置保存至数据库 * 4 若下载过程中突然断电,下次下载时则从数据库中取出每条线程 * 的断点值,继续下载即可 * * 注意事项: * 1 示例中的图片链接选自凤凰网,图片版权属于凤凰网(http://www.ifeng.com/) * 2 该图片以后可能会被凤凰网删除,所以在测试时可选择其他网络图片 */public class MainActivity extends Activity { private Context mContext; private EditText mUrlEditText; private ProgressBar mProgressBar; private Button mDownLoadButton; private TextView mPercentTextView; private UIHandler mHandler=new UIHandler(); private final int NORMAL=9527; private final int ERROR=250; private final String MESSAGE_KEY="size"; private DownloadProgressListener mDownloadProgressListener; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); init(); } private void init() {mContext = this;mUrlEditText = (EditText) findViewById(R.id.urlEditText);mProgressBar = (ProgressBar) findViewById(R.id.progressBar);mPercentTextView = (TextView) findViewById(R.id.percentTextView);mDownLoadButton = (Button) findViewById(R.id.downloadButton);mDownLoadButton.setOnClickListener(new ClickListenerImpl());mDownloadProgressListener=new DownloadProgressListenerImpl();} private class ClickListenerImpl implements OnClickListener{@Override public void onClick(View v) {if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){String path=mUrlEditText.getText().toString();download(path,Environment.getExternalStorageDirectory());}else{Toast.makeText(mContext, R.string.SDCardError, Toast.LENGTH_SHORT).show();} } } private void download(String path,File saveDir){ new Thread(new DownloadRunnableImpl(path, saveDir)).start(); } //下载文件的子线程 private class DownloadRunnableImpl implements Runnable{ private String path; private File SaveDir; public DownloadRunnableImpl(String path, File saveDir) {this.path = path;this.SaveDir = saveDir; } public void run(){ try {FileDownloader fileDownloader=new FileDownloader(getApplicationContext(), path, 4, SaveDir);mProgressBar.setMax(fileDownloader.getFileRawSize());//置显示进度的回调fileDownloader.setDownloadProgressListener(mDownloadProgressListener);//开始下载fileDownloader.startDownload();} catch (Exception e) {Message message=new Message();message.what=ERROR;mHandler.sendMessage(message);e.printStackTrace();} } }private class DownloadProgressListenerImpl implements DownloadProgressListener {@Overridepublic void onDownloadSize(int size) {Message message = new Message();message.what = NORMAL;message.getData().putInt(MESSAGE_KEY, size);mHandler.sendMessage(message);}}// 显示下载进度private class UIHandler extends Handler {public void handleMessage(Message msg) {switch (msg.what) {case NORMAL:int size = msg.getData().getInt(MESSAGE_KEY);mProgressBar.setProgress(size);float percentFloat = (float) (mProgressBar.getProgress() / (float) mProgressBar.getMax());int percentInt = (int) (percentFloat * 100);mPercentTextView.setText(percentInt + "%");System.out.println("size="+size+",mProgressBar.getProgress()="+mProgressBar.getProgress()+",mProgressBar.getMax()="+mProgressBar.getMax());if (mProgressBar.getProgress() == mProgressBar.getMax()) {Toast.makeText(mContext, R.string.success,Toast.LENGTH_SHORT).show();}break;case ERROR:Toast.makeText(mContext, R.string.error,Toast.LENGTH_SHORT).show();break;default:break;}}} }
DownloadProgressListener如下:
package cc.download;/** * 下载的监听接口 */public interface DownloadProgressListener { public void onDownloadSize(int size);}
FileDownloader如下:
package cc.download;import java.io.File;import java.io.RandomAccessFile;import java.net.HttpURLConnection;import java.net.URL;import java.util.HashMap;import java.util.Map;import java.util.UUID;import java.util.regex.Matcher;import java.util.regex.Pattern;import org.apache.http.HttpStatus;import android.content.Context;import cc.helper.DownloadThreadHelper;/** * 该类用于下载文件 * 思路梳理: * 1 在FileDownloader的构造方法里做一些下载的准备操作 * 1.1 获取源文件的大小 * 1.2 计算每条线程需要下载的数据长度 * 1.3 生成本地文件用于存储下载的文件并设置本地RandomAccessFile的大小 * 1.4 取出每条线程的上次下载的情况和已下载的总长度 * * 2 利用startDownload()方法执行文件下载 * * 分析说明: * 关于1.2 计算每条线程需要下载的数据长度 的原理及影响的详细分析: * everyThreadNeedDownloadLength=rawFileSize%threadNum==0 ? rawFileSize/threadNum : rawFileSize/threadNum+1; * 1 如果资源大小模于线程数时结果为0,那么表示每条线程需要下载的大小恰好将原大小等分 * 2 当然更多的情况是有余数的(即不能整除).那么此时该怎么办呢?每条线程该下载的长度是多少呢? * 我们可以这么做: * 2.1 原大小/除以线程的条数 * 2.2 在2.1的基础上+1 * 即代码rawFileSize/threadNum+1 * 这样就表示每条线程要下载的大小长度 * * 带来的问题: * 按照上面的方式,我们期望每条线程下载相同的数据量.但是存在个小问题: * 这样各线程累加起来的的下载总量是要大于原大小的.一般会多几个字节. * 这几个字节是多余的(redundant). * 而且这些几个多余的字节是出现在最后一条下载线程中,它的终止位置已经 * 超过了原文件的末尾. * 这样就造成了下载的文件与原文件大小不一致. * 所以在下载过程中需要处理该情况,处理方式参见DownloadThread类 * */public class FileDownloader {private Context mContext;//下载路径 private String mDownloadPath; //待下载文件的原始长度 private int rawFileSize=0; //保存下载文件的本地文件 private File mLocalFile; //已下载大小 private int downloadTotalSize=0; //下载此文件需要的各个线程 private DownloadThread [] downloadThreadsArray; //每条线程需下载的长度 private int everyThreadNeedDownloadLength; //存放目前每条线程的信息包含其id和已下载大小 private Map<Integer,Integer> mCurrentEveryThreadInfoMap; //用于对各个线程进行操作 private DownloadThreadHelper mDownloadThreadHelper; private DownloadProgressListener mDownloadProgressListener; public FileDownloader(Context context,String downloadPath,int threadNum,File fileSaveDir){ System.out.println("源文件路径 downloadPath= "+downloadPath); System.out.println("下载开启的线程数 threadNum="+threadNum); try {mContext=context;mDownloadPath=downloadPath;mCurrentEveryThreadInfoMap=new HashMap<Integer,Integer>();mDownloadThreadHelper=new DownloadThreadHelper(context);downloadThreadsArray=new DownloadThread[threadNum];URL downloadUrl=new URL(downloadPath);HttpURLConnection httpURLConnection=(HttpURLConnection) downloadUrl.openConnection();httpURLConnection.setReadTimeout(5*1000);httpURLConnection.setRequestMethod("GET");httpURLConnection.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, */*");httpURLConnection.setRequestProperty("Accept-Language", "zh-CN");httpURLConnection.setRequestProperty("Referer", downloadPath); httpURLConnection.setRequestProperty("Charset", "UTF-8");httpURLConnection.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)");httpURLConnection.setRequestProperty("Connection", "Keep-Alive");httpURLConnection.connect();if(httpURLConnection.getResponseCode()==HttpStatus.SC_OK){//第一步:获得源文件大小rawFileSize=httpURLConnection.getContentLength();System.out.println("源文件大小 rawFileSize="+rawFileSize);//第二步:计算每条线程需要下载的数据长度everyThreadNeedDownloadLength=rawFileSize%threadNum==0 ? rawFileSize/threadNum : rawFileSize/threadNum+1;System.out.println("每条线程应下载大小 everyThreadNeedDownloadLength="+everyThreadNeedDownloadLength);if(rawFileSize<=0){throw new RuntimeException("file is not found");}//第三步:建立本地文件并设置本地RandomAccessFile的大小String rawFileName=getFileName(httpURLConnection);if(!fileSaveDir.exists()){fileSaveDir.mkdirs();}mLocalFile=new File(fileSaveDir, rawFileName);System.out.println("本地文件路径 mLocalFile.getAbsolutePath()="+mLocalFile.getAbsolutePath());RandomAccessFile randomAccessFile=new RandomAccessFile(mLocalFile, "rw");if(rawFileSize>0){randomAccessFile.setLength(rawFileSize);}randomAccessFile.close();//第四步:取出每条线程的上次下载的情况和已下载的总长度/** * 以下操作围绕断点进行的: * 1 从数据库取出每条线程上一次的下载情况,存入everyThreadLastDownloadLengthMap * 2 判断上次下载时开启的线程数是否和本次下载开启线程数一致 * 若不一致则无法在原基础上继续断点下载,则将mCurrentEveryThreadInfoMap中各条线程下载量设置为0 * 若一致则取出已下载的数据总量 *///若以前下载过,则取出每条线程以前的情况存入mCurrentEveryThreadInfoMapMap<Integer,Integer> everyThreadLastDownloadLengthMap=mDownloadThreadHelper.getEveryThreadDownloadLength(downloadPath);if(everyThreadLastDownloadLengthMap.size()>0){for(Map.Entry<Integer, Integer> entry:everyThreadLastDownloadLengthMap.entrySet()){mCurrentEveryThreadInfoMap.put(entry.getKey(), entry.getValue());System.out.println("--> 断点回复处 --> threadID="+entry.getKey()+",已下载数据量 length="+entry.getValue());}}//若以往的线程条数和现在的线程条数不一致,则无法按照//断点继续下载.所以将每条已下载的数据量置为0,并更新数据库//若以往的线程条数和现在的线程条数一致,则取出已下载的数据总量if(downloadThreadsArray.length!=mCurrentEveryThreadInfoMap.size()){mCurrentEveryThreadInfoMap.clear();for(int i=1;i<=downloadThreadsArray.length;i++){ mCurrentEveryThreadInfoMap.put(i, 0);}mDownloadThreadHelper.saveEveryThreadDownloadLength(mDownloadPath, mCurrentEveryThreadInfoMap);}else{for(int i=1;i<=threadNum;i++){downloadTotalSize=downloadTotalSize+mCurrentEveryThreadInfoMap.get(i);}// 更新已经下载的数据量if (mDownloadProgressListener != null) {mDownloadProgressListener.onDownloadSize(downloadTotalSize);}System.out.println("--> 断点回复处 --> 已经下载 downloadTotalSize="+downloadTotalSize);}}else{throw new RuntimeException("The HttpURLConnection is fail");}} catch (Exception e) {throw new RuntimeException("Init FileDownloader is fail");} } public int startDownload( ){ try {URL downloadURL=new URL(mDownloadPath);/** * 对每条线程的下载情况进行判断 * 如果没有下载完,则继续下载 * 否则将该线程置为空 */for(int i=1;i<=downloadThreadsArray.length;i++){//取出此线程已经下载的大小int existDownloadSize=mCurrentEveryThreadInfoMap.get(i);if(existDownloadSize<everyThreadNeedDownloadLength && downloadTotalSize<rawFileSize){downloadThreadsArray[i-1]=new DownloadThread(this, i, everyThreadNeedDownloadLength, mCurrentEveryThreadInfoMap.get(i), downloadURL, mLocalFile);//设置优先级downloadThreadsArray[i-1].setPriority(7);//开始下载,注意数组的开始角标是从零开始的downloadThreadsArray[i-1].start();}else{downloadThreadsArray[i-1]=null;}}/** * 注意: * 对于下载失败的线程(-1)重新开始下载 */Boolean isAllFinish=true;while (isAllFinish) {isAllFinish = false;for (int i = 1; i <= downloadThreadsArray.length; i++) {if (downloadThreadsArray[i - 1] != null && !downloadThreadsArray[i - 1].isFinish()) {isAllFinish = true;if (downloadThreadsArray[i - 1].getDownloadSize() == -1) {downloadThreadsArray[i - 1] = new DownloadThread(this, i, everyThreadNeedDownloadLength,mCurrentEveryThreadInfoMap.get(i), downloadURL,mLocalFile);downloadThreadsArray[i - 1].setPriority(7);downloadThreadsArray[i - 1].start();}}}}//下载完成,删除记录mDownloadThreadHelper.deleteEveryThreadDownloadRecord(mDownloadPath);} catch (Exception e) {throw new RuntimeException("the download is fail");} return downloadTotalSize; } //获取线程数 public int getThreadsNum(){ return downloadThreadsArray.length; } //获取原始文件大小 public int getFileRawSize(){ return rawFileSize; } //更新已经下载的总数据量 protected synchronized void appendDownloadTotalSize(int newSize){ downloadTotalSize=downloadTotalSize+newSize; if (mDownloadProgressListener!=null) { mDownloadProgressListener.onDownloadSize(downloadTotalSize);} System.out.println("当前总下载量 downloadTotalSize="+downloadTotalSize); } //更新每条线程已经下载的数据量 protected synchronized void updateEveryThreadDownloadLength(int threadid,int position){ mCurrentEveryThreadInfoMap.put(threadid, position); mDownloadThreadHelper.updateEveryThreadDownloadLength(mDownloadPath, mCurrentEveryThreadInfoMap); } //获取文件名 public String getFileName(HttpURLConnection conn){ String filename = mDownloadPath.substring(mDownloadPath.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;} public void setDownloadProgressListener(DownloadProgressListener downloadProgressListener){ mDownloadProgressListener=downloadProgressListener; }}
DownloadThread如下:
package cc.download;import java.io.File;import java.io.InputStream;import java.io.RandomAccessFile;import java.net.HttpURLConnection;import java.net.URL;/** * 该类表示每一条进行下载的线程 * 在下载的过程中不断地实时更新该线程的已下载量,从而刷新每条线程的已下载量 * 亦实时更新已经下载的总数据量 * * 分析说明 * 1 在FileDownloader中提到最后一条线程下载的数据可能有redundant数据的问题 * 在此描述解决办法如下: * 在计算每条线程下载终止位置(endPosition)时,需要对最后一条线程做特殊的处理 * 当endPosition大于了原文件的大小,即代码endPosition>fileDownloader.getFileRawSize() * 此时需要修改该线程的结束位置endPosition和应该下载的数据量everyThreadNeedDownloadLength * * 2 遇到的一个问题 * 在此处我们为每条线程设置了读取网络数据的范围,即代码: * httpURLConnection.setRequestProperty("Range","bytes="+startPosition+"-"+endPosition); * 然后在InputStreamwhile不断读取数据的时候,采用的方式是: * ((len=inputStream.read(b))!=-1)来判断是否已经读到了endPosition. * 之所以这么做是依据以往的经验且以为设置了Range,所以在读到endPosition的时候就应该返回-1 * 但是这么做是错误的!!!!!!!!!! * 在读到endPosition的时候并没有返回-1,会一直往下读取数据导致错误. * 所以: * 需要给while()多加一个判断downLength<everyThreadNeedDownloadLength * 表示当前该线程已经下载的数据量小于它本该读取的数据量,此时才可以继续读数据. * * 请注意另外一个重要问题: * 每条线程最后一次读取可能会多读数据的问题. * 比如:还有1000个字节就完成了该线程本该操作的数据,但是inputStream在接下来的最后一次 * 读数据时候却读了缓存byte [] b=new byte[1024]大小的字节数,造成了redundant数据的问题 * 产生这个问题的原因还是因为在读到endPosition的时候并没有返回-1,所以会一直往下读取数. * 所以在此需要特殊处理: * 2.1 修正downLength即代码:downLength=everyThreadNeedDownloadLength; * 2.2 修正实际下载有用的数据的长度即代码: * fileDownloader.appendDownloadTotalSize(len-redundant); * 而不是fileDownloader.appendDownloadTotalSize(len) * 从而确保了DownloadTotalSize值的准确 */public class DownloadThread extends Thread{ private FileDownloader fileDownloader; private int threadId; private int everyThreadNeedDownloadLength; private int downLength; private URL downUrl; private File localFile; private Boolean isFinish=false; public DownloadThread(FileDownloader fileDownloader, int threadId, int everyThreadNeedDownloadLength,int downLength, URL downUrl, File localFile) {this.fileDownloader = fileDownloader;this.threadId = threadId;this.everyThreadNeedDownloadLength = everyThreadNeedDownloadLength;this.downLength = downLength;this.downUrl = downUrl;this.localFile = localFile;}@Overridepublic void run() { try { //当此线程已下载量小于应下载量if(downLength<everyThreadNeedDownloadLength){HttpURLConnection httpURLConnection=(HttpURLConnection) downUrl.openConnection();httpURLConnection.setConnectTimeout(5*1000);httpURLConnection.setRequestMethod("GET");httpURLConnection.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, */*");httpURLConnection.setRequestProperty("Accept-Language", "zh-CN");httpURLConnection.setRequestProperty("Referer", downUrl.toString()); httpURLConnection.setRequestProperty("Charset", "UTF-8");//开始下载位置int startPosition=everyThreadNeedDownloadLength*(threadId-1)+downLength;//结束下载位置int endPosition=everyThreadNeedDownloadLength*threadId-1;//处理最后一条下载线程的特殊情况if (endPosition>fileDownloader.getFileRawSize()) {int redundant=endPosition-(fileDownloader.getFileRawSize()-1);endPosition=fileDownloader.getFileRawSize()-1;everyThreadNeedDownloadLength=everyThreadNeedDownloadLength-redundant;}//设置下载的起止位置httpURLConnection.setRequestProperty("Range","bytes="+startPosition+"-"+endPosition);System.out.println("====> 每条线程的下载起始情况 threadId="+threadId+",startPosition="+startPosition+",endPosition="+endPosition);httpURLConnection.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)");httpURLConnection.setRequestProperty("Connection", "Keep-Alive");RandomAccessFile randomAccessFile=new RandomAccessFile(localFile, "rwd");randomAccessFile.seek(startPosition);InputStream inputStream=httpURLConnection.getInputStream();int len=0;byte [] b=new byte[1024];while((len=inputStream.read(b))!=-1 && downLength<everyThreadNeedDownloadLength){downLength=downLength+len;int redundant =0;//处理每条线程最后一次读取可能会多读数据的问题if (downLength>everyThreadNeedDownloadLength) {redundant =downLength-everyThreadNeedDownloadLength;downLength=everyThreadNeedDownloadLength;}randomAccessFile.write(b, 0, len-redundant);//实时更新该线程的已下载量,从而刷新每条线程的已下载量fileDownloader.updateEveryThreadDownloadLength(threadId, downLength);//实时更新已经下载的总量fileDownloader.appendDownloadTotalSize(len-redundant);}inputStream.close();randomAccessFile.close();//改变标志位isFinish=true;}} catch (Exception e) {downLength=-1;e.printStackTrace();}} //判断是否已经下载完成 public Boolean isFinish(){ return isFinish; } //返回该线程已经下载的数据量,若-1则代表失败 public int getDownloadSize(){return downLength; } }
DBOpenHelper如下:
package cc.helper;import android.content.Context;import android.database.sqlite.SQLiteDatabase;import android.database.sqlite.SQLiteOpenHelper;/** * 总结: * 1 继承自SQLiteOpenHelper * 2 完成构造方法 */public class DBOpenHelper extends SQLiteOpenHelper {private final static String DBName = "download.db";private final static int VERSION = 1;public DBOpenHelper(Context context) {super(context, DBName, null, VERSION);}@Overridepublic void onCreate(SQLiteDatabase db) {db.execSQL("CREATE TABLE IF NOT EXISTS filedownload(id integer primary key autoincrement, downpath varchar(100), threadid INTEGER, downlength INTEGER)"); }@Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {db.execSQL("DROP TABLE IF EXISTS filedownload");onCreate(db);}}
DownloadThreadHelper如下:
package cc.helper;import java.util.HashMap;import java.util.Map;import android.content.Context;import android.database.Cursor;import android.database.sqlite.SQLiteDatabase;/** * 该类主要用来操作每条线程对应的保存在数据库中的下载信息 * 比如:某条线程已经下载的数据量 */public class DownloadThreadHelper {private DBOpenHelper dbOpenHelper;public DownloadThreadHelper(Context context) {dbOpenHelper = new DBOpenHelper(context);} /** * 保存每条线程已经下载的长度 */public void saveEveryThreadDownloadLength(String path, Map<Integer, Integer> map) {SQLiteDatabase db = dbOpenHelper.getWritableDatabase();db.beginTransaction();try {for (Map.Entry<Integer, Integer> entry : map.entrySet()) {db.execSQL("insert into filedownload(downpath, threadid, downlength) values(?,?,?)", new Object[] { path, entry.getKey(), entry.getValue() });}db.setTransactionSuccessful();} finally { db.endTransaction(); db.close();}}/** * 获取每条线程已经下载的长度 */public Map<Integer, Integer> getEveryThreadDownloadLength(String path){SQLiteDatabase db = dbOpenHelper.getWritableDatabase();Cursor cursor=db.rawQuery("select threadid,downlength from filedownload where downpath=?", new String[]{path});Map<Integer, Integer> threadsMap=new HashMap<Integer, Integer>();while(cursor.moveToNext()){int threadid=cursor.getInt(0);int downlength=cursor.getInt(1);threadsMap.put(threadid, downlength);}cursor.close();db.close();return threadsMap;}/** * 实时更新每条线程已经下载的数据长度 * 利用downPath和threadid来确定其已下载长度 */public void updateEveryThreadDownloadLength(String path, Map<Integer, Integer> map) {SQLiteDatabase db = dbOpenHelper.getWritableDatabase();db.beginTransaction();try {for (Map.Entry<Integer, Integer> entry : map.entrySet()) {db.execSQL("update filedownload set downlength=? where threadid=? and downpath=?",new Object[] { entry.getValue(), entry.getKey(), path}); System.out.println("更新该线程下载情况 threadID="+entry.getKey()+",length="+entry.getValue());}db.setTransactionSuccessful();} finally { db.endTransaction(); db.close();}} /** * 下载完成后,删除每条线程的记录 */public void deleteEveryThreadDownloadRecord(String path){SQLiteDatabase db = dbOpenHelper.getWritableDatabase();db.execSQL("delete from filedownload where downpath=?", new String[]{path});db.close();}}
main.xml如下:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="文件下载路径:" /> <EditText android:id="@+id/urlEditText" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="http://y1.ifengimg.com/dc14f57c79882c4a/2013/1003/re_524cb964403c1.jpg" /> <Button android:id="@+id/downloadButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="下载" /> <ProgressBar android:id="@+id/progressBar" style="?android:attr/progressBarStyleHorizontal" android:layout_width="fill_parent" android:layout_height="18dip" /> <TextView android:id="@+id/percentTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" /></LinearLayout>