android开发过程中,下载是必备的功能,下载安装包,或者下载图片,假设用户下载过程中人为中断网络,或者网络不稳定中断下载任务,好的用户体验是从断开的地方继续下载,而不是又从头开始下载,因为比方说用户是拿4g来下载的,你一个游戏安装包100多M,用户下载了90M,突然手机没电了,充好电,又从头下载,那岂不是浪费用户的流量。所以断点续传是非常必要的一个功能。其实断点续传也可以使用多线程来实现的,本篇先不写的这么麻烦了,就单线程去下载一个任务了,如果中断了,下次再点击下载的时候,从断点继续下载。好,开始我们的实验。本实验是下载一个安装包。比如我们下载360手机卫士。给出Demo代码。
1、activity_resume_download.xml 下载页面
<span style="font-size:18px;"><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="断点续传测试" /> <Button android:id="@+id/btn_download" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="开始下载" /> <Button android:id="@+id/btn_cancel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="取消下载" /> <ProgressBar android:id="@+id/progressBar" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="20dp" android:layout_gravity="center_horizontal" /> </LinearLayout></span>
2、ResumeDownloadActivity.java
package com.figo.study.activity; import android.app.Activity; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Log; import android.view.View; import android.widget.ProgressBar; import android.widget.Toast; import com.figo.study.R; import com.figo.study.mgr.DownloadMgr; import com.figo.study.utils.FileUtils; import java.io.File; public class ResumeDownloadActivity extends Activity implements View.OnClickListener { String tag = "ResumeDownloadActivity"; ProgressBar mProgressBar; String downloadUrl = "http://msoftdl.360.cn/mobilesafe/shouji360/360safe/500192/360MobileSafe.apk"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_resume_download); findViewById(R.id.btn_download).setOnClickListener(this); findViewById(R.id.btn_cancel).setOnClickListener(this); mProgressBar = (ProgressBar) findViewById(R.id.progressBar); } private final Handler msgHandler = new Handler() { public void handleMessage(Message msg) { switch (msg.what) { case 0: Toast.makeText(getApplicationContext(), msg.getData().get("msg").toString(), Toast.LENGTH_SHORT).show(); break; default: break; } } }; @Override public void onClick(View view) { switch (view.getId()) { case R.id.btn_download: String directory = FileUtils.getStorageDirectory(); String fileName = directory + File.separator + getFileName(downloadUrl); DownloadMgr.getInstance().addTask(downloadUrl, fileName, new DownloadMgr.Callback() { @Override public void onProgress(long progress, long total) { super.onProgress(progress, total); mProgressBar.setProgress((int) (100 * progress / total)); } @Override public void onStart() { super.onStart(); sendMsg("start"); } @Override public void onSuccess() { super.onSuccess(); Log.i(tag, "success"); sendMsg("success"); } @Override public void onFailed(boolean cancelled, String msg) { super.onFailed(cancelled, msg); Log.e(tag, msg); //Looper.getMainLooper().prepare();//这么干虽然可以在子线程,弹出toast,但是子线程执行到这里,后面的代码将不再执行 // Toast.makeText(ResumeDownloadActivity.this, "download start", Toast.LENGTH_SHORT).show(); //Looper.getMainLooper().loop(); //进程间通信还是用Handler比较靠谱 sendMsg(msg); } }); break; case R.id.btn_cancel: DownloadMgr.getInstance().cancelTask(downloadUrl); break; } } private String getFileName(String downloadUrl) { return downloadUrl.substring(downloadUrl.lastIndexOf("/")); } private void sendMsg(String msg) { Message msgNew = new Message(); msgNew.what = 0; Bundle bundle = new Bundle(); bundle.putString("msg", msg); msgNew.setData(bundle); msgHandler.sendMessage(msgNew); } }
3、下载工具类DownloadMgr.java
package com.figo.study.mgr; import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkInfo; import com.figo.study.activity.MyApplication; import com.figo.study.utils.CommonUtil; import com.figo.study.utils.IOUtil; import java.io.BufferedInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.io.InterruptedIOException; import java.net.HttpURLConnection; import java.net.SocketTimeoutException; import java.net.URL; import java.util.HashMap; import java.util.concurrent.Executor; import java.util.concurrent.Executors; /** * Created by figo on 16/7/25. */ public class DownloadMgr { private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; Executor mExecutor = Executors.newFixedThreadPool(MAXIMUM_POOL_SIZE); static DownloadMgr mDownloadMgr; static Object obj = new Object(); HashMap<String, DownloadTask> mTasks = new HashMap<String, DownloadTask>(); public static void init() { getInstance(); } public static DownloadMgr getInstance() { synchronized (obj) { if (mDownloadMgr == null) { mDownloadMgr = new DownloadMgr(); } } return mDownloadMgr; } public void addTask(String downloadUrl, String filePath, Callback callback) { if (!mTasks.containsKey(downloadUrl)) { mTasks.put(downloadUrl, new DownloadTask(downloadUrl, filePath, callback)); } mTasks.get(downloadUrl).startDownload(); } public void removeTask(String downloadUrl, String filePath, Callback callback) { if (mTasks.containsKey(downloadUrl)) { mTasks.get(downloadUrl).cancel(); } mTasks.remove(downloadUrl); } public class DownloadTask implements Runnable { private String downloadUrl; private String filePath; Callback callback; public DownloadTask(String downloadUrl, String filePath, Callback callback) { this.downloadUrl = downloadUrl; this.filePath = filePath; this.callback = callback; } public void startDownload() { mExecutor.execute(this); } @Override public void run() { runResumable(downloadUrl, filePath, callback); } synchronized boolean cancel() { if (thread == null) return false; thread.interrupt(); return true; } } Thread thread; public void runResumable(String downloadUrl, String filePath, Callback callback) { Thread.currentThread().setPriority(Thread.MIN_PRIORITY); thread = Thread.currentThread(); final Context ctx = MyApplication.getInstance(); String msg = ""; boolean interrupted = false; HttpURLConnection conn = null; long resumePosition = 0; final File file = new File(filePath); try { //20160720 add final File parent = file.getParentFile(); if (!parent.exists()) { parent.mkdirs(); } if (!file.exists()) { file.createNewFile(); } callback.onStart(); //简单一点就用md5来校验 // if (file.exists() && StringUtil.equalsIgnoreCase(md5, Md5.md5(file))) { // suc = true; // return; // } //非wifi环境不下载 if (!isWifiActive(ctx)) { msg = "请在wifi环境下下载"; callback.onFailed(true, msg); return; } resumePosition = file.exists() ? file.length() : 0; // Create connection object conn = (HttpURLConnection) new URL(downloadUrl).openConnection(); conn.setConnectTimeout(60000); conn.setReadTimeout(60000); conn.setDoInput(true); conn.setUseCaches(false); // Make the request conn.setRequestMethod("GET"); conn.setRequestProperty("User-Agent", "Java/Android"); conn.setRequestProperty("Connection", "close"); conn.setRequestProperty("Http-version", "HTTP/1.1"); conn.setRequestProperty("Cache-Control", "no-transform"); if (resumePosition > 0) { //断点续传的关键设置Range conn.setRequestProperty("Range", "bytes=" + resumePosition + "-"); } conn.connect(); final int responseCode = conn.getResponseCode(); if (responseCode == 416) { msg = "已经下载!"; callback.onFailed(true, msg); return; } if (responseCode != 200 && responseCode != 206) { msg = "网络繁忙,请稍后再试!"; callback.onFailed(true, msg); return; } long fileLength = conn.getContentLength(); InputStream is = new BufferedInputStream(conn.getInputStream()); FileOutputStream fos = new FileOutputStream(file, resumePosition > 0); try { int read = 0; long progress = resumePosition; byte[] buffer = new byte[4096 * 2]; while ((read = is.read(buffer)) > 0 && !(interrupted = Thread.interrupted())) { try { fos.write(buffer, 0, read); } catch (Exception e) { msg = "磁盘空间已满,无法下载"; throw e; } // progress progress += read; callback.onProgress(progress, fileLength); } } finally { IOUtil.closeQuietly(fos); IOUtil.closeQuietly(is); } //20160720 resumable download if (file.exists()) { //也可以通过md5来校验 // if (StringUtil.equalsIgnoreCase(md5, Md5.md5(file))) { // suc = true; // return; // } //检验数据是否完整 if (file.length() == fileLength + resumePosition) { callback.onSuccess(); return; } } } catch (Exception e) { interrupted = interrupted || Thread.interrupted() || (e instanceof InterruptedIOException && !(e instanceof SocketTimeoutException)); msg = "网络异常,下载失败"; if (interrupted) { msg = "下载被中断!"; } callback.onFailed(true, msg); } finally { disconnect(conn); } } static void disconnect(HttpURLConnection conn) { try { if (conn == null) return; conn.disconnect(); } catch (Throwable e) { e.printStackTrace(); } } public static abstract class Callback { public void onStart() { } public void onProgress(long progress, long total) { } public void onSuccess() { } public void onFailed(boolean cancelled, String msg) { } } public boolean isWifiActive(Context ctx) { try { ConnectivityManager mgr = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo info = mgr.getActiveNetworkInfo(); return (info != null) ? info.getType() == ConnectivityManager.TYPE_WIFI : false; } catch (Exception e) { return false; } } public void cancelAllTask() { try { if (mTasks != null) { for (String taskKey : mTasks.keySet()) { mTasks.get(taskKey).cancel(); } } } catch (Exception e) { CommonUtil.printStackTrace(e); } } public void cancelTask(String key) { try { if (mTasks != null) { mTasks.get(key).cancel(); } } catch (Exception e) { CommonUtil.printStackTrace(e); } } public static boolean checkNetAvailable(Context ctx) { try { ConnectivityManager mgr = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo info = mgr.getActiveNetworkInfo(); return (info != null) ? true : false; } catch (Exception e) { return true; } } }