您的位置:首页 > 理论基础 > 计算机网络

[Android]网络资源下载时断点续传的实现

2011-06-09 23:13 477 查看
断点续传用到的知识点:

1.使用RandomAccessFile设定文件大小并于指定位置开始读数据[randomAccessFile.seek(position)]。

2.请求资源链接时指定所请求数据的返回范围。

httpURLConnection.setRequestProperty("Range", "bytes=" + start + "-" + (contentLength - 1));



效果图如下[CSDN]:



(相当抱歉,这个动画的时间太长了)

以下代码中的NetworkTool为通过个人编程经验封装好的网络工具类,强力推荐,当然也欢迎拍砖。

使用NetworkTool访问一个网络链接并获取数据的小示例为:

HttpURLConnection httpConn = NetworkTool.openUrl(context, url);
int respondCode = NetworkTool.connect(httpConn);
if (respondCode == HttpURLConnection.HTTP_OK) {
byte[] data = NetworkTool.fetchData_doClose(httpConn);
String content = new String(data);
data = null;
// parse content
} else {
// handles something
}
NetworkTool.disconnect(httpConn);


代码中的DontPressWithParentButton可用于ListView中,当点击该Button时不会触发ListView中的OnItemClickListener。实现方法为重写Button的setPressed(boolean)方法:

@Override
	public void setPressed(boolean pressed) {
		if (pressed && ((View) getParent()).isPressed()) {
			return;
		}
		super.setPressed(pressed);
	}




代码如下:

lab.sodino.downloadbreak.ActDownload.java



package lab.sodino.downloadbreak;
import java.io.File;
import java.text.DecimalFormat;
import lab.sodino.downloadbreak.bean.BeanDownload;
import lab.sodino.downloadbreak.util.LogOut;
import lab.sodino.downloadbreak.util.NetworkTool;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
public class ActDownload extends Activity {
	/** 下载存放地:"/sdcard/sodino/"。 */
	public static final String RES_LOAD_FOLDER = File.separator + "sdcard" + File.separator
			+ "sodino" + File.separator;
	/** 刷新进度。 */
	public static final int REFRESH = 1;
	public static final int CODE = 10;
	private BeanDownload bean;
	private TextView txtName;
	private TextView txtProgress;
	private TextView txtSize;
	private ProgressBar progressBar;
	private Button btnAction;
	private Handler handler;
	private BtnListener btnListener;
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.l_download);
		initBeanDownload();
		initViews$Handler();
	}
	private void initBeanDownload() {
		bean = new BeanDownload();
		bean.name = "微信.apk";
		// 请找个可以无需跳转直接下载的地址
		bean.url = "http://XXOO.com/weixin20android16.apk";
		bean.state = BeanDownload.STATE_INTERRUPTED;
		bean.size = bean.loadedSize = 0l;
		bean.enable = true;
	}
	private void initViews$Handler() {
		txtName = (TextView) findViewById(R.id.txtName);
		txtName.setText(bean.name);
		txtProgress = (TextView) findViewById(R.id.txtProgress);
		txtProgress.setText(getProgressTxt(bean));
		txtSize = (TextView) findViewById(R.id.txtSize);
		txtSize.setText(formatSizeTxt(bean.size));
		progressBar = (ProgressBar) findViewById(R.id.progressBar);
		progressBar.setProgress(getProgressInt(bean, progressBar.getMax()));
		btnListener = new BtnListener();
		btnAction = (Button) findViewById(R.id.btnAction);
		btnAction.setOnClickListener(btnListener);
		btnAction.setText(getTxt(bean));
		btnAction.setEnabled(isEnable(bean));
		// handler
		handler = new Handler() {
			public void handleMessage(Message msg) {
				txtProgress.setText(getProgressTxt(bean));
				txtSize.setText(formatSizeTxt(bean.size));
				progressBar.setProgress(getProgressInt(bean, progressBar.getMax()));
				btnAction.setText(getTxt(bean));
				btnAction.setEnabled(isEnable(bean));
			}
		};
	}
	private void pauseDownload() {
		bean.enable = false;
		handler.sendEmptyMessage(REFRESH);
	}
	private void doDownload() {
		handler.sendEmptyMessage(REFRESH);
		new DownloadThread().start();
	}
	private void reloadDownload() {
		bean.size = bean.loadedSize = 0;
		bean.enable = true;
		doDownload();
	}
	private void installDownload() {
		Intent intent = new Intent(Intent.ACTION_VIEW);
		String filePath = RES_LOAD_FOLDER + bean.name;
		intent.setDataAndType(Uri.parse("file://" + filePath),
				"application/vnd.android.package-archive");
		// 如果仅是简单的startActivity(intent),会造成onCreate()再执行一次。
		ActDownload.this.startActivityForResult(intent, CODE);
	}
	class BtnListener implements Button.OnClickListener {
		public void onClick(View v) {
			LogOut.out(this, "state:" + bean.state);
			switch (bean.state) {
			case BeanDownload.STATE_LOADING:
				// 点击了"暂停"
				pauseDownload();
				break;
			case BeanDownload.STATE_INTERRUPTED:
				// 点击了"继续"
				doDownload();
				break;
			case BeanDownload.STATE_DOWNLOAD_FAIL:
				// 点击了"重载"
				reloadDownload();
				break;
			case BeanDownload.STATE_COMPLETED:
				// 点击了"安装"
				installDownload();
				break;
			}
		}
	}
	class DownloadThread extends Thread {
		public void run() {
			bean.state = BeanDownload.STATE_LOADING;
			bean.enable = true;
			NetworkTool.download2File(ActDownload.this, bean, handler);
			LogOut.out(this, "size:" + bean.size + " loaded:" + bean.loadedSize + " enable:"
					+ bean.enable);
			// 测试“重载”请释放下面代码的注释然后等待下载正常结束
			// bean.loadedSize = 0;
			if (bean.size > 0 && bean.loadedSize == bean.size) {
				String localPath = RES_LOAD_FOLDER + bean.name;
				File tmpFile = new File(localPath + ".tmp");
				tmpFile.renameTo(new File(localPath));
				bean.enable = false;
				bean.state = BeanDownload.STATE_COMPLETED;
			} else {
				if (bean.enable == false) {
					bean.state = BeanDownload.STATE_INTERRUPTED;
				} else {
					bean.state = BeanDownload.STATE_DOWNLOAD_FAIL;
				}
			}
			LogOut.out(this, "state=" + bean.state);
			handler.sendEmptyMessage(REFRESH);
		}
	}
	public static String getProgressTxt(BeanDownload bean) {
		String resStr = "0%";
		if (bean.size != 0) {
			double result = bean.loadedSize * 1.0 / bean.size;
			DecimalFormat decFormat = new DecimalFormat("#.#%");
			resStr = decFormat.format(result);
		}
		return resStr;
	}
	private String formatSizeTxt(long size) {
		String sizeTxt = "未知";
		if (size > 0) {
			size = size >> 10;
			sizeTxt = String.valueOf(size) + "k";
		}
		return sizeTxt;
	}
	public static int getProgressInt(BeanDownload bean, int max) {
		int result = (bean.size > 0) ? (int) (bean.loadedSize * max * 1.0 / bean.size) : 0;
		return result;
	}
	private String getTxt(BeanDownload bean) {
		String txt = "安装";
		switch (bean.state) {
		case BeanDownload.STATE_COMPLETED:
			txt = "安装";
			break;
		case BeanDownload.STATE_LOADING:
			txt = "暂停";
			break;
		case BeanDownload.STATE_INTERRUPTED:
			txt = "继续";
			break;
		case BeanDownload.STATE_DOWNLOAD_FAIL:
			txt = "重载";
			break;
		}
		return txt;
	}
	private boolean isEnable(BeanDownload bean) {
		boolean enable = true;
		if (bean.enable == false && bean.state == BeanDownload.STATE_LOADING) {
			enable = false;
		}
		return enable;
	}
}



lab.sodino.downloadbreak.bean.BeanDownload.java

package lab.sodino.downloadbreak.bean;
/**
 * @author Sodino E-mail:sodinoopen@hotmail.com
 * @version Time:2011-6-8 下午11:33:10
 */
public class BeanDownload {
	/** 正在下载数据。Button应显示“暂停”。 */
	public static final int STATE_LOADING = 0;
	/** 数据全部下载完成。Button应显示“安装”。 */
	public static final int STATE_COMPLETED = 1;
	/** 数据下载过程中被暂停。Button应显示“继续”。 */
	public static final int STATE_INTERRUPTED = 2;
	/** 下载安装包失败。Button应显示“失败”。 */
	public static final int STATE_DOWNLOAD_FAIL = 3;
	public String name;
	public long size;
	public long loadedSize;
	public String url;
	public int state;
	public boolean enable;
}




lab.sodino.downloadbreak.ui.DontPressWithParentButton.java

package lab.sodino.downloadbreak.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Button;
/**
* @author Sodino E-mail:sodinoopen@hotmail.com
* @version Time:2011-6-5 下午08:37:27
*/
public class DontPressWithParentButton extends Button {
public DontPressWithParentButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override public void setPressed(boolean pressed) { if (pressed && ((View) getParent()).isPressed()) { return; } super.setPressed(pressed); }
}



lab.sodino.downloadbreak.util.NetworkTool.java

package lab.sodino.downloadbreak.util;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URL;
import lab.sodino.downloadbreak.ActDownload;
import lab.sodino.downloadbreak.bean.BeanDownload;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Handler;
/**
 * 管理联网操作,包括管理url参数、下载APK包、获取任务字符串。<br/>
 * 
 * @author Sodino E-mail:sodinoopen@hotmail.com
 * @version Time:2011-4-6 下午03:42:50
 */
public class NetworkTool {
	/**
	 * 开启一个HTTP链接。
	 */
	public static HttpURLConnection openUrl(Context context, String urlStr) {
		LogOut.out("Network", "urlStr[" + urlStr + "]");
		URL urlURL = null;
		HttpURLConnection httpConn = null;
		try {
			urlURL = new URL(urlStr);
			// 需要android.permission.ACCESS_NETWORK_STATE
			// 在没有网络的情况下,返回值为null。
			NetworkInfo networkInfo = ((ConnectivityManager) context
					.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
			// 如果是使用的运营商网络
			if (networkInfo != null) {
				if (networkInfo.getType() == ConnectivityManager.TYPE_MOBILE) {
					// 获取默认代理主机ip
					String host = android.net.Proxy.getDefaultHost();
					// 获取端口
					int port = android.net.Proxy.getDefaultPort();
					if (host != null && port != -1) {
						// 封装代理連接主机IP与端口号。
						InetSocketAddress inetAddress = new InetSocketAddress(host, port);
						// 根据URL链接获取代理类型,本链接适用于TYPE.HTTP
						java.net.Proxy.Type proxyType = java.net.Proxy.Type.valueOf(urlURL
								.getProtocol().toUpperCase());
						java.net.Proxy javaProxy = new java.net.Proxy(proxyType, inetAddress);
						httpConn = (HttpURLConnection) urlURL.openConnection(javaProxy);
					} else {
						httpConn = (HttpURLConnection) urlURL.openConnection();
					}
				} else {
					httpConn = (HttpURLConnection) urlURL.openConnection();
				}
				httpConn.setDoInput(true);
			} else {
				// LogOut.out(this, "No Avaiable Network");
			}
		} catch (NullPointerException npe) {
			npe.printStackTrace();
		} catch (MalformedURLException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return httpConn;
	}
	/** 启动链接并将RespondCode值返回。 */
	public static int connect(HttpURLConnection httpConn) {
		int code = -1;
		if (httpConn != null) {
			try {
				httpConn.connect();
				code = httpConn.getResponseCode();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		LogOut.out("NetworkTool", "respond_code=" + code);
		return code;
	}
	/**
	 * 将指定的HTTP链接内容存储到指定的的文件中。<br/>
	 * 返回值仅当参考。<br/>
	 * 
	 * @param httpConn
	 * @param filePath
	 *            指定存储的文件路径。
	 */
	public static boolean download2File(HttpURLConnection httpConn, String filePath) {
		boolean result = true;
		File file = new File(filePath);
		FileOutputStream fos = null;
		byte[] data = new byte[1024];
		int readLength = -1;
		InputStream is = null;
		try {
			fos = new FileOutputStream(file);
			is = httpConn.getInputStream();
			while ((readLength = is.read(data)) != -1) {
				fos.write(data, 0, readLength);
				fos.flush();
			}
			fos.flush();
		} catch (IOException ie) {
			result = false;
			ie.printStackTrace();
		} finally {
			try {
				if (is != null) {
					is.close();
				}
				if (fos != null) {
					fos.close();
				}
			} catch (IOException ie) {
				ie.printStackTrace();
			}
		}
		return result;
	}
	/**
	 * 将bean资源下载。<br/>
	 * 支持断点续传。
	 * 
	 */
	public static void download2File(Context context, BeanDownload bean, Handler handler) {
		String filePath = ActDownload.RES_LOAD_FOLDER + bean.name + ".tmp";
		HttpURLConnection httpConn = null;
		File file = new File(filePath);
		RandomAccessFile randomFile = null;
		FileOutputStream fos = null;
		int dataBlockLength = 2048;
		byte[] data = new byte[dataBlockLength];
		int readLength = -1;
		InputStream is = null;
		try {
			if (bean.size <= 0) {
				bean.loadedSize = 0;
				if (file.getParentFile().exists() == false) {
					file.getParentFile().mkdirs();
				}
				if (file.exists() == false) {
					file.createNewFile();
				}
				// 采用普通的下载方式
				fos = new FileOutputStream(file);
				httpConn = openUrl(context, bean.url);
				int respondCode = connect(httpConn);
				LogOut.out("NetworkTool", "respondCode=" + respondCode);
				if (respondCode == HttpURLConnection.HTTP_OK) {
					bean.size = httpConn.getContentLength();
					is = httpConn.getInputStream();
					while ((readLength = is.read(data)) != -1 && bean.enable) {
						fos.write(data, 0, readLength);
						bean.loadedSize += readLength;
						handler.sendEmptyMessage(ActDownload.REFRESH);
					}
				}
			} else {
				// 采用断点续传方式
				randomFile = new RandomAccessFile(file, "rw");
				randomFile.setLength(bean.size);
				httpConn = openUrl(context, bean.url);
				httpConn.setRequestProperty("Range", "bytes=" + bean.loadedSize + "-"
						+ (bean.size - 1));
				int respondCode = connect(httpConn);
				if (respondCode == HttpURLConnection.HTTP_PARTIAL) {
					is = httpConn.getInputStream();
					while ((readLength = is.read(data)) != -1 && bean.enable) {
						randomFile.seek(bean.loadedSize);
						randomFile.write(data, 0, readLength);
						bean.loadedSize += readLength;
						handler.sendEmptyMessage(ActDownload.REFRESH);
					}
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				if (is != null) {
					is.close();
				}
				if (httpConn != null) {
					disconnect(httpConn);
				}
				if (fos != null) {
					fos.close();
				}
				if (randomFile != null) {
					randomFile.close();
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
	/** 读取HttpURLConnection的数据并关闭相关流。 */
	public static byte[] fetchData_doClose(HttpURLConnection httpConn) {
		byte[] data = null;
		ByteArrayOutputStream baos = null;
		InputStream is = null;
		int read = -1;
		try {
			baos = new ByteArrayOutputStream();
			is = httpConn.getInputStream();
			while ((read = is.read()) != -1) {
				baos.write(read);
			}
			data = baos.toByteArray();
		} catch (IOException ie) {
			ie.printStackTrace();
		} finally {
			try {
				if (is != null) {
					is.close();
				}
				if (baos != null) {
					baos.close();
				}
				if (httpConn != null) {
					httpConn.disconnect();
				}
			} catch (IOException ie) {
				ie.printStackTrace();
			}
		}
		return data;
	}
	public static void disconnect(HttpURLConnection httpConn) {
		if (httpConn != null) {
			httpConn.disconnect();
		}
	}
}





/res/drawable/l_download.xml



<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="horizontal"
	android:layout_width="fill_parent"
	android:layout_height="wrap_content"
	android:layout_marginTop="20dip"
	android:layout_marginBottom="0dip">
	<LinearLayout android:layout_width="fill_parent"
		android:layout_height="wrap_content"
		android:orientation="vertical"
		android:layout_weight="1"
		android:layout_marginLeft="10dip"
		android:layout_marginRight="10dip">
		<LinearLayout android:layout_width="fill_parent"
			android:layout_height="wrap_content"
			android:layout_marginTop="10dip"
			android:layout_marginBottom="10dip"
			android:orientation="horizontal">
			<TextView android:layout_width="wrap_content"
				android:layout_height="wrap_content"
				android:layout_weight="1"
				android:id="@+id/txtName"
				android:singleLine="true"
				android:ellipsize="middle"
				android:textColor="#ffffffff"
				android:textSize="15sp"
			></TextView>
			<TextView android:layout_width="wrap_content"
				android:layout_height="wrap_content"
				android:id="@+id/txtProgress"
				android:textColor="#ffffffff"
				android:textSize="10sp"
				android:layout_marginRight="10dip"
			></TextView>
			<TextView android:layout_width="wrap_content"
				android:layout_height="wrap_content"
				android:id="@+id/txtSize"
				android:textColor="#ffffffff"
				android:textSize="10sp"
			></TextView>
		</LinearLayout>
		<ProgressBar android:layout_width="fill_parent"
			android:layout_height="wrap_content"
			android:id="@+id/progressBar"
		    style="?android:attr/progressBarStyleHorizontal" mce_style="?android:attr/progressBarStyleHorizontal"
		></ProgressBar>
	</LinearLayout>
	<lab.sodino.downloadbreak.ui.DontPressWithParentButton
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		android:minWidth="60dip"
		android:minHeight="30dip"
		android:id="@+id/btnAction"
		android:textColor="#ff000000"
		android:textStyle="bold"
		android:layout_marginRight="5dip"
		android:layout_marginLeft="5dip"
		android:layout_marginTop="10dip"
		android:layout_marginBottom="10dip"
		android:focusable="false"
		android:focusableInTouchMode="false"
	></lab.sodino.downloadbreak.ui.DontPressWithParentButton>
</LinearLayout>




所要添加的权限:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />




本文内容归CSDN博客博主Sodino 所有

转载请注明出处: http://blog.csdn.net/sodino/archive/2011/06/09/6535278.aspx





附:HTTP1.1 Range与Content-Range范例说明

假设你要开发一个多线程下载工具,你会自然的想到把文件分割成多个部分,比如4个部分,然后创建4个线程,每个线程负责下载一个部分,如果文件大小为403个byte,那么你的分割方式可以为:0-99 (前100个字节),100-199(第二个100字节),200-299(第三个100字节),300-402(最后103个字节)。

分割完成,每个线程都明白自己的任务,比如线程3的任务是负责下载200-299这部分文件,现在的问题是:线程3发送一个什么样的请求报文,才能够保证只请求文件的200-299字节,而不会干扰其他线程的任务。这时,我们可以使用HTTP1.1的Range头。Range头域可以请求实体的一个或者多个子范围,Range的值为0表示第一个字节,也就是Range计算字节数是从0开始的:

表示头500个字节:Range: bytes=0-499

表示第二个500字节:Range: bytes=500-999

表示最后500个字节:Range: bytes=-500

表示500字节以后的范围:Range: bytes=500-

第一个和最后一个字节:Range: bytes=0-0,-1

同时指定几个范围:Range: bytes=500-600,601-999

所以,线程3发送的请求报文必须有这一行:

Range: bytes=200-299

服务器接收到线程3的请求报文,发现这是一个带有Range头的GET请求,如果一切正常,服务器的响应报文会有下面这行:

HTTP/1.1 206 OK

表示处理请求成功,响应报文还有这一行:

Content-Range: bytes 200-299/403

斜杠后面的403表示文件的大小,通常Content-Range的用法为:

. The first 500 bytes:

Content-Range: bytes 0-499/1234

. The second 500 bytes:

Content-Range: bytes 500-999/1234

. All except for the first 500 bytes:

Content-Range: bytes 500-1233/1234

. The last 500 bytes:

Content-Range: bytes 734-1233/1234



From:http://blogold.chinaunix.net/u3/94343/showart.php?id=1891855
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: