Android断点续传原理及实现

常见两种网络请求方式

一、 HttpURLConnection
HttpURLConnection的setRequestProperty()方法,对我们要读取的字节部分进行控制,比如:
···
1.Range=0-100代表只读取前100个字节。
2.Range=100-500代表读取从第100个字节开始,读到第500个字节为止。
3.Range=100-则代表从第100个字节开始读取,一直读取到文件末尾结束。

···
断点续传过程
1、在暂停时记录一下已经读取到的位置,在重新开始的时候利用setRequestProperty()方法设置一下我们要读取的字节位置即可
2、RandomAccessFile来进行字节流的写入,RandomAccessFile有一个方法seek(long),允许我们指定要写入的位置

多线程断点续传实现原理相同,只是利用多个线程同时下载,每个线程指定要下载的字节部分,写入到文件的指定部分

二、 okhttp
1、调用请求获取文件长度
2、先判断文件是否找到,找到代表已经下载过,则获取其长度 判断长度是否大于文件长度 大于则创建新文件继续下载或者下载终止
3、设置请求头请求范围

//确定下载的范围,添加此头,则服务器就可以跳过已经下载好的部分  
.addHeader("RANGE", "bytes=" + downloadLength + "-" + contentLength) 

4、存储时候设置:RandomAccessFile这个类,但是如果不涉及到多个部分拼接的话是没必要的,直接使用输出流就好了,在输出流的构造方法上添加一个true的参数,代表是在原文件的后面添加数据即可三 、 httpurlconnection 示例

class SingleDownloadTask{private String filePath;private int contentLength;private int readLength;private RandomAccessFile file=null;private boolean isPause=false;private URL url;public boolean isDownloading() {return isDownloading;}private boolean isDownloading=false;private Handler mHandler=new Handler();public SingleDownloadTask(String urlStr,String filePath) {this.filePath=filePath;readLength=0;contentLength=0;try {url=new URL(urlStr);} catch (MalformedURLException e) {e.printStackTrace();}}//download from pospublic void download(){new Thread(new Runnable() {@Overridepublic void run() {isDownloading=true;HttpURLConnection conn=null;InputStream is=null;BufferedInputStream bis=null;try {file=new RandomAccessFile(filePath,"rw");file.seek(readLength);conn= (HttpURLConnection) url.openConnection();if(readLength==0){contentLength=conn.getContentLength();}else{conn.setRequestProperty("Range","bytes="+readLength+"-");}is=conn.getInputStream();bis=new BufferedInputStream(is);byte[] bytes=new byte[1024];int len=0;while (!isPause&&((len=bis.read(bytes,0,1024)))!=-1){file.write(bytes,0,len);readLength+=len;mHandler.post(new Runnable() {@Overridepublic void run() {float rate=((float)readLength)/contentLength;mProgressBar.setProgress((int) (100*rate));mTextView.setText((int)(100*rate)+"%");}});}isDownloading=false;if (readLength>=contentLength){mHandler.post(new Runnable() {@Overridepublic void run() {Toast.makeText(MainActivity.this, "文件下载成功,保存在"+filePath, Toast.LENGTH_SHORT).show();mImageView.setImageBitmap(BitmapFactory.decodeFile(filePath));}});}file.close();} catch (MalformedURLException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}finally {if (bis!=null){try {bis.close();if (is!=null){is.close();}if (conn!=null){conn.disconnect();}} catch (IOException e) {e.printStackTrace();}}}}}).start();}public void start(){isPause=false;download();}public void pause(){isPause=true;}}

OKHttp3是如今非常流行的Android网络请求框架,那么如何利用Android实现断点续传呢,今天写了个Demo尝试了一下,感觉还是有点意思

准备阶段

我们会用到OKHttp3来做网络请求,使用RxJava来实现线程的切换,并且开启Java8来启用Lambda表达式,毕竟RxJava实现线程切换非常方便,而且数据流的形式也非常舒服,同时Lambda和RxJava配合食用味道更佳
打开我们的app Module下的build.gradle,代码如下

apply plugin: 'com.android.application'  android {  compileSdkVersion 24  buildToolsVersion "24.0.3"  defaultConfig {  applicationId "com.lanou3g.downdemo"  minSdkVersion 15  targetSdkVersion 24  versionCode 1  versionName "1.0"  testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"  //为了开启Java8  jackOptions{  enabled true;  }  }  buildTypes {  release {  minifyEnabled false  proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'  }  }  //开启Java1.8 能够使用lambda表达式  compileOptions{  sourceCompatibility JavaVersion.VERSION_1_8  targetCompatibility JavaVersion.VERSION_1_8  }  
}  dependencies {  compile fileTree(dir: 'libs', include: ['*.jar'])  androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {  exclude group: 'com.android.support', module: 'support-annotations'  })  compile 'com.android.support:appcompat-v7:24.1.1'  testCompile 'junit:junit:4.12'  //OKHttp  compile 'com.squareup.okhttp3:okhttp:3.6.0'  //RxJava和RxAndroid 用来做线程切换的  compile 'io.reactivex.rxjava2:rxandroid:2.0.1'  compile 'io.reactivex.rxjava2:rxjava:2.0.1'  
}  

OKHttp和RxJava,RxAndroid使用的都是最新的版本,并且配置开启了Java8

布局文件

接着开始书写布局文件

<?xml version="1.0" encoding="utf-8"?>  
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  xmlns:tools="http://schemas.android.com/tools"  android:id="@+id/activity_main"  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="com.lanou3g.downdemo.MainActivity">  <LinearLayout  android:layout_width="match_parent"  android:layout_height="wrap_content"  android:orientation="horizontal">  <ProgressBar  android:id="@+id/main_progress1"  android:layout_width="0dp"  android:layout_weight="1"  android:layout_height="match_parent"  style="@style/Widget.AppCompat.ProgressBar.Horizontal" />  <Button  android:id="@+id/main_btn_down1"  android:layout_width="wrap_content"  android:layout_height="wrap_content"  android:text="下载1"/>  <Button  android:id="@+id/main_btn_cancel1"  android:layout_width="wrap_content"  android:layout_height="wrap_content"  android:text="取消1"/>  </LinearLayout>  <LinearLayout  android:layout_width="match_parent"  android:layout_height="wrap_content"  android:orientation="horizontal">  <ProgressBar  android:id="@+id/main_progress2"  android:layout_width="0dp"  android:layout_weight="1"  android:layout_height="match_parent"  style="@style/Widget.AppCompat.ProgressBar.Horizontal" />  <Button  android:id="@+id/main_btn_down2"  android:layout_width="wrap_content"  android:layout_height="wrap_content"  android:text="下载2"/>  <Button  android:id="@+id/main_btn_cancel2"  android:layout_width="wrap_content"  android:layout_height="wrap_content"  android:text="取消2"/>  </LinearLayout>  <LinearLayout  android:layout_width="match_parent"  android:layout_height="wrap_content"  android:orientation="horizontal">  <ProgressBar  android:id="@+id/main_progress3"  android:layout_width="0dp"  android:layout_weight="1"  android:layout_height="match_parent"  style="@style/Widget.AppCompat.ProgressBar.Horizontal" />  <Button  android:id="@+id/main_btn_down3"  android:layout_width="wrap_content"  android:layout_height="wrap_content"  android:text="下载3"/>  <Button  android:id="@+id/main_btn_cancel3"  android:layout_width="wrap_content"  android:layout_height="wrap_content"  android:text="取消3"/>  </LinearLayout>  
</LinearLayout>  

3个ProgressBar就是为了显示进度的,每个ProgressBar对应2个Button,一个是开始下载,一个是暂停(取消)下载,这里需要说明的是,对下载来说暂停和取消没有什么区别,除非当取消的时候,会顺带把临时文件都删除了,在本例里是不区分他俩的.

Application

我们这里需要用到一些文件路径,有一个全局Context会比较方便, 而Application也是Context的子类,使用它的是最方便的,所以我们写一个类来继承Application

package com.lanou3g.downdemo;  import android.app.Application;  
import android.content.Context;  /** * Created by 陈丰尧 on 2017/2/2. */  public class MyApp extends Application {  public static Context sContext;//全局的Context对象  @Override  public void onCreate() {  super.onCreate();  sContext = this;  }  
}  

可以看到,我们就是要获得一个全局的Context对象的
我们在AndroidManifest中注册一下我们的Application,同时再把我们所需要的权限给上

<?xml version="1.0" encoding="utf-8"?>  
<manifest xmlns:android="http://schemas.android.com/apk/res/android"  package="com.lanou3g.downdemo">  <!--网络权限-->  <uses-permission android:name="android.permission.INTERNET"/>  <application  android:allowBackup="true"  android:icon="@mipmap/ic_launcher"  android:label="@string/app_name"  android:supportsRtl="true"  android:name=".MyApp"  android:theme="@style/AppTheme">  <activity android:name=".MainActivity">  <intent-filter>  <action android:name="android.intent.action.MAIN" />  <category android:name="android.intent.category.LAUNCHER" />  </intent-filter>  </activity>  </application>  </manifest>  

我们只需要一个网络权限,在application标签下,添加name属性,来指向我们的Application

DownloadManager

接下来是核心代码了,就是我们的DownloadManager,先上代码

package com.lanou3g.downdemo;  import java.io.File;  
import java.io.FileOutputStream;  
import java.io.IOException;  
import java.io.InputStream;  
import java.util.HashMap;  
import java.util.concurrent.atomic.AtomicReference;  import io.reactivex.Observable;  
import io.reactivex.ObservableEmitter;  
import io.reactivex.ObservableOnSubscribe;  
import io.reactivex.android.schedulers.AndroidSchedulers;  
import io.reactivex.schedulers.Schedulers;  
import okhttp3.Call;  
import okhttp3.OkHttpClient;  
import okhttp3.Request;  
import okhttp3.Response;  /** * Created by 陈丰尧 on 2017/2/2. */  public class DownloadManager {  private static final AtomicReference<DownloadManager> INSTANCE = new AtomicReference<>();  private HashMap<String, Call> downCalls;//用来存放各个下载的请求  private OkHttpClient mClient;//OKHttpClient;  //获得一个单例类  public static DownloadManager getInstance() {  for (; ; ) {  DownloadManager current = INSTANCE.get();  if (current != null) {  return current;  }  current = new DownloadManager();  if (INSTANCE.compareAndSet(null, current)) {  return current;  }  }  }  private DownloadManager() {  downCalls = new HashMap<>();  mClient = new OkHttpClient.Builder().build();  }  /** * 开始下载 * * @param url              下载请求的网址 * @param downLoadObserver 用来回调的接口 */  public void download(String url, DownLoadObserver downLoadObserver) {  Observable.just(url)  .filter(s -> !downCalls.containsKey(s))//call的map已经有了,就证明正在下载,则这次不下载  .flatMap(s -> Observable.just(createDownInfo(s)))  .map(this::getRealFileName)//检测本地文件夹,生成新的文件名  .flatMap(downloadInfo -> Observable.create(new DownloadSubscribe(downloadInfo)))//下载  .observeOn(AndroidSchedulers.mainThread())//在主线程回调  .subscribeOn(Schedulers.io())//在子线程执行  .subscribe(downLoadObserver);//添加观察者  }  public void cancel(String url) {  Call call = downCalls.get(url);  if (call != null) {  call.cancel();//取消  }  downCalls.remove(url);  }  /** * 创建DownInfo * * @param url 请求网址 * @return DownInfo */  private DownloadInfo createDownInfo(String url) {  DownloadInfo downloadInfo = new DownloadInfo(url);  long contentLength = getContentLength(url);//获得文件大小  downloadInfo.setTotal(contentLength);  String fileName = url.substring(url.lastIndexOf("/"));  downloadInfo.setFileName(fileName);  return downloadInfo;  }  private DownloadInfo getRealFileName(DownloadInfo downloadInfo) {  String fileName = downloadInfo.getFileName();  long downloadLength = 0, contentLength = downloadInfo.getTotal();  File file = new File(MyApp.sContext.getFilesDir(), fileName);  if (file.exists()) {  //找到了文件,代表已经下载过,则获取其长度  downloadLength = file.length();  }  //之前下载过,需要重新来一个文件  int i = 1;  while (downloadLength >= contentLength) {  int dotIndex = fileName.lastIndexOf(".");  String fileNameOther;  if (dotIndex == -1) {  fileNameOther = fileName + "(" + i + ")";  } else {  fileNameOther = fileName.substring(0, dotIndex)  + "(" + i + ")" + fileName.substring(dotIndex);  }  File newFile = new File(MyApp.sContext.getFilesDir(), fileNameOther);  file = newFile;  downloadLength = newFile.length();  i++;  }  //设置改变过的文件名/大小  downloadInfo.setProgress(downloadLength);  downloadInfo.setFileName(file.getName());  return downloadInfo;  }  private class DownloadSubscribe implements ObservableOnSubscribe<DownloadInfo> {  private DownloadInfo downloadInfo;  public DownloadSubscribe(DownloadInfo downloadInfo) {  this.downloadInfo = downloadInfo;  }  @Override  public void subscribe(ObservableEmitter<DownloadInfo> e) throws Exception {  String url = downloadInfo.getUrl();  long downloadLength = downloadInfo.getProgress();//已经下载好的长度  long contentLength = downloadInfo.getTotal();//文件的总长度  //初始进度信息  e.onNext(downloadInfo);  Request request = new Request.Builder()  //确定下载的范围,添加此头,则服务器就可以跳过已经下载好的部分  .addHeader("RANGE", "bytes=" + downloadLength + "-" + contentLength)  .url(url)  .build();  Call call = mClient.newCall(request);  downCalls.put(url, call);//把这个添加到call里,方便取消  Response response = call.execute();  File file = new File(MyApp.sContext.getFilesDir(), downloadInfo.getFileName());  InputStream is = null;  FileOutputStream fileOutputStream = null;  try {  is = response.body().byteStream();  fileOutputStream = new FileOutputStream(file, true);  byte[] buffer = new byte[2048];//缓冲数组2kB  int len;  while ((len = is.read(buffer)) != -1) {  fileOutputStream.write(buffer, 0, len);  downloadLength += len;  downloadInfo.setProgress(downloadLength);  e.onNext(downloadInfo);  }  fileOutputStream.flush();  downCalls.remove(url);  } finally {  //关闭IO流  IOUtil.closeAll(is, fileOutputStream);  }  e.onComplete();//完成  }  }  /** * 获取下载长度 * * @param downloadUrl * @return */  private long getContentLength(String downloadUrl) {  Request request = new Request.Builder()  .url(downloadUrl)  .build();  try {  Response response = mClient.newCall(request).execute();  if (response != null && response.isSuccessful()) {  long contentLength = response.body().contentLength();  response.close();  return contentLength == 0 ? DownloadInfo.TOTAL_ERROR : contentLength;  }  } catch (IOException e) {  e.printStackTrace();  }  return DownloadInfo.TOTAL_ERROR;  }  }  

代码稍微有点长,关键部位我都加了注释了,我们挑关键地方看看
首先我们这个类是单例类,我们下载只需要一个OKHttpClient就足够了,所以我们让构造方法私有,而单例类的获取实例方法就是这个getInstance();当然大家用别的方式实现单例也可以的,然后我们在构造方法里初始化我们的HttpClient,并且初始化一个HashMap,用来放所有的网络请求的,这样当我们取消下载的时候,就可以找到url对应的网络请求然后把它取消掉就可以了
接下来就是核心的download方法了,首先是参数,第一个参数url不用多说,就是请求的网址,第二个参数是一个Observer对象,因为我们使用的是RxJava,并且没有特别多复杂的方法,所以就没单独写接口,而是谢了一个Observer对象来作为回调,接下来是DownLoadObserver的代码

package com.lanou3g.downdemo;  import io.reactivex.Observer;  
import io.reactivex.disposables.Disposable;  /** * Created by 陈丰尧 on 2017/2/2. */  public  abstract class DownLoadObserver implements Observer<DownloadInfo> {  protected Disposable d;//可以用于取消注册的监听者  protected DownloadInfo downloadInfo;  @Override  public void onSubscribe(Disposable d) {  this.d = d;  }  @Override  public void onNext(DownloadInfo downloadInfo) {  this.downloadInfo = downloadInfo;  }  @Override  public void onError(Throwable e) {  e.printStackTrace();  }  }  

在RxJava2中 这个Observer有点变化,当注册观察者的时候,会调用onSubscribe方法,而该方法参数就是用来取消注册的,这样的改动可以更灵活的有监听者来取消监听了,我们的进度信息会一直的传送的onNext方法里,这里将下载所需要的内容封了一个类叫DownloadInfo

package com.lanou3g.downdemo;  /** * Created by 陈丰尧 on 2017/2/2. * 下载信息 */  public class DownloadInfo {  public static final long TOTAL_ERROR = -1;//获取进度失败  private String url;  private long total;  private long progress;  private String fileName;  public DownloadInfo(String url) {  this.url = url;  }  public String getUrl() {  return url;  }  public String getFileName() {  return fileName;  }  public void setFileName(String fileName) {  this.fileName = fileName;  }  public long getTotal() {  return total;  }  public void setTotal(long total) {  this.total = total;  }  public long getProgress() {  return progress;  }  public void setProgress(long progress) {  this.progress = progress;  }  
}  

这个类就是一些基本信息,total就是需要下载的文件的总大小,而progress就是当前下载的进度了,这样就可以计算出下载的进度信息了
接着看DownloadManager的download方法,首先通过url生成一个Observable对象,然后通过filter操作符过滤一下,如果当前正在下载这个url对应的内容,那么就不下载它,
接下来调用createDownInfo重新生成Observable对象,这里应该用map也是可以的,createDownInfo这个方法里会调用getContentLength来获取服务器上的文件大小,可以看一下这个方法的代码,

/** * 获取下载长度 * * @param downloadUrl * @return */  private long getContentLength(String downloadUrl) {  Request request = new Request.Builder()  .url(downloadUrl)  .build();  try {  Response response = mClient.newCall(request).execute();  if (response != null && response.isSuccessful()) {  long contentLength = response.body().contentLength();  response.close();  return contentLength == 0 ? DownloadInfo.TOTAL_ERROR : contentLength;  }  } catch (IOException e) {  e.printStackTrace();  }  return DownloadInfo.TOTAL_ERROR;  }  

可以看到,其实就是在通过OK进行了一次网络请求,并且从返回的头信息里拿到文件的大小信息,一般这个信息都是可以拿到的,除非下载网址不是直接指向资源文件的,而是自己手写的Servlet,那就得跟后台人员沟通好了.注意,这次网络请求并没有真正的去下载文件,而是请求个大小就结束了,具体原因会在后面真正请求数据的时候解释
接着download方法
获取完文件大小后,就可以去硬盘里找文件了,这里调用了getRealFileName方法

private DownloadInfo getRealFileName(DownloadInfo downloadInfo) {  String fileName = downloadInfo.getFileName();  long downloadLength = 0, contentLength = downloadInfo.getTotal();  File file = new File(MyApp.sContext.getFilesDir(), fileName);  if (file.exists()) {  //找到了文件,代表已经下载过,则获取其长度  downloadLength = file.length();  }  //之前下载过,需要重新来一个文件  int i = 1;  while (downloadLength >= contentLength) {  int dotIndex = fileName.lastIndexOf(".");  String fileNameOther;  if (dotIndex == -1) {  fileNameOther = fileName + "(" + i + ")";  } else {  fileNameOther = fileName.substring(0, dotIndex)  + "(" + i + ")" + fileName.substring(dotIndex);  }  File newFile = new File(MyApp.sContext.getFilesDir(), fileNameOther);  file = newFile;  downloadLength = newFile.length();  i++;  }  //设置改变过的文件名/大小  downloadInfo.setProgress(downloadLength);  downloadInfo.setFileName(file.getName());  return downloadInfo;  }  

这个方法就是看本地是否有已经下载过的文件,如果有,再判断一次本地文件的大小和服务器上数据的大小,如果是一样的,证明之前下载全了,就再成一个带(1)这样的文件,而如果本地文件大小比服务器上的小的话,那么证明之前下载了一半断掉了,那么就把进度信息保存上,并把文件名也存上,看完了再回到download方法
之后就开始真正的网络请求了,这里写了一个内部类来实现ObservableOnSubscribe接口,这个接口也是RxJava2的,东西和之前一样,好像只改了名字,看一下代码

private class DownloadSubscribe implements ObservableOnSubscribe<DownloadInfo> {  private DownloadInfo downloadInfo;  public DownloadSubscribe(DownloadInfo downloadInfo) {  this.downloadInfo = downloadInfo;  }  @Override  public void subscribe(ObservableEmitter<DownloadInfo> e) throws Exception {  String url = downloadInfo.getUrl();  long downloadLength = downloadInfo.getProgress();//已经下载好的长度  long contentLength = downloadInfo.getTotal();//文件的总长度  //初始进度信息  e.onNext(downloadInfo);  Request request = new Request.Builder()  //确定下载的范围,添加此头,则服务器就可以跳过已经下载好的部分  .addHeader("RANGE", "bytes=" + downloadLength + "-" + contentLength)  .url(url)  .build();  Call call = mClient.newCall(request);  downCalls.put(url, call);//把这个添加到call里,方便取消  Response response = call.execute();  File file = new File(MyApp.sContext.getFilesDir(), downloadInfo.getFileName());  InputStream is = null;  FileOutputStream fileOutputStream = null;  try {  is = response.body().byteStream();  fileOutputStream = new FileOutputStream(file, true);  byte[] buffer = new byte[2048];//缓冲数组2kB  int len;  while ((len = is.read(buffer)) != -1) {  fileOutputStream.write(buffer, 0, len);  downloadLength += len;  downloadInfo.setProgress(downloadLength);  e.onNext(downloadInfo);  }  fileOutputStream.flush();  downCalls.remove(url);  } finally {  //关闭IO流  IOUtil.closeAll(is, fileOutputStream);  }  e.onComplete();//完成  }  }  

主要看subscribe方法
首先拿到url,当前进度信息和文件的总大小,然后开始网络请求,注意这次网络请求的时候需要添加一条头信息

.addHeader("RANGE", "bytes=" + downloadLength + "-" + contentLength)

这条头信息的意思是下载的范围是多少,downloadLength是从哪开始下载,contentLength是下载到哪,当要断点续传的话必须添加这个头,让输入流跳过多少字节的形式是不行的,所以我们要想能成功的添加这条信息那么就必须对这个url请求2次,一次拿到总长度,来方便判断本地是否有下载一半的数据,第二次才开始真正的读流进行网络请求,我还想了一种思路,当文件没有下载完成的时候添加一个自定义的后缀,当下载完成再把这个后缀取消了,应该就不需要请求两次了.
接下来就是正常的网络请求,向本地写文件了,而写文件到本地这,网上大多用的是RandomAccessFile这个类,但是如果不涉及到多个部分拼接的话是没必要的,直接使用输出流就好了,在输出流的构造方法上添加一个true的参数,代表是在原文件的后面添加数据即可,而在循环里,不断的调用onNext方法发送进度信息,当写完了之后别忘了关流,同时把call对象从hashMap中移除了.这里写了一个IOUtil来关流

package com.lanou3g.downdemo;  import java.io.Closeable;  
import java.io.IOException;  /** * Created by 陈丰尧 on 2017/2/2. */  public class IOUtil {  public static void closeAll(Closeable... closeables){  if(closeables == null){  return;  }  for (Closeable closeable : closeables) {  if(closeable!=null){  try {  closeable.close();  } catch (IOException e) {  e.printStackTrace();  }  }  }  }  
}  

其实就是挨一个判断是否为空,并关闭罢了
这样download方法就完成了,剩下的就是切换线程,注册观察者了

MainActivity

最后是aty的代码

package com.lanou3g.downdemo;  import android.net.Uri;  
import android.support.annotation.IdRes;  
import android.support.v7.app.AppCompatActivity;  
import android.os.Bundle;  
import android.view.View;  
import android.widget.Button;  
import android.widget.ProgressBar;  
import android.widget.Toast;  public class MainActivity extends AppCompatActivity implements View.OnClickListener {  private Button downloadBtn1, downloadBtn2, downloadBtn3;  private Button cancelBtn1, cancelBtn2, cancelBtn3;  private ProgressBar progress1, progress2, progress3;  private String url1 = "http://192.168.31.169:8080/out/dream.flac";  private String url2 = "http://192.168.31.169:8080/out/music.mp3";  private String url3 = "http://192.168.31.169:8080/out/code.zip";  @Override  protected void onCreate(Bundle savedInstanceState) {  super.onCreate(savedInstanceState);  setContentView(R.layout.activity_main);  downloadBtn1 = bindView(R.id.main_btn_down1);  downloadBtn2 = bindView(R.id.main_btn_down2);  downloadBtn3 = bindView(R.id.main_btn_down3);  cancelBtn1 = bindView(R.id.main_btn_cancel1);  cancelBtn2 = bindView(R.id.main_btn_cancel2);  cancelBtn3 = bindView(R.id.main_btn_cancel3);  progress1 = bindView(R.id.main_progress1);  progress2 = bindView(R.id.main_progress2);  progress3 = bindView(R.id.main_progress3);  downloadBtn1.setOnClickListener(this);  downloadBtn2.setOnClickListener(this);  downloadBtn3.setOnClickListener(this);  cancelBtn1.setOnClickListener(this);  cancelBtn2.setOnClickListener(this);  cancelBtn3.setOnClickListener(this);  }  @Override  public void onClick(View v) {  switch (v.getId()) {  case R.id.main_btn_down1:  DownloadManager.getInstance().download(url1, new DownLoadObserver() {  @Override  public void onNext(DownloadInfo value) {  super.onNext(value);  progress1.setMax((int) value.getTotal());  progress1.setProgress((int) value.getProgress());  }  @Override  public void onComplete() {  if(downloadInfo != null){  Toast.makeText(MainActivity.this,  downloadInfo.getFileName() + "-DownloadComplete",  Toast.LENGTH_SHORT).show();  }  }  });  break;  case R.id.main_btn_down2:  DownloadManager.getInstance().download(url2, new DownLoadObserver() {  @Override  public void onNext(DownloadInfo value) {  super.onNext(value);  progress2.setMax((int) value.getTotal());  progress2.setProgress((int) value.getProgress());  }  @Override  public void onComplete() {  if(downloadInfo != null){  Toast.makeText(MainActivity.this,  downloadInfo.getFileName() + Uri.encode("下载完成"),  Toast.LENGTH_SHORT).show();  }  }  });  break;  case R.id.main_btn_down3:  DownloadManager.getInstance().download(url3, new DownLoadObserver() {  @Override  public void onNext(DownloadInfo value) {  super.onNext(value);  progress3.setMax((int) value.getTotal());  progress3.setProgress((int) value.getProgress());  }  @Override  public void onComplete() {  if(downloadInfo != null){  Toast.makeText(MainActivity.this,  downloadInfo.getFileName() + "下载完成",  Toast.LENGTH_SHORT).show();  }  }  });  break;  case R.id.main_btn_cancel1:  DownloadManager.getInstance().cancel(url1);  break;  case R.id.main_btn_cancel2:  DownloadManager.getInstance().cancel(url2);  break;  case R.id.main_btn_cancel3:  DownloadManager.getInstance().cancel(url3);  break;  }  }  private <T extends View> T bindView(@IdRes int id){  View viewById = findViewById(id);  return (T) viewById;  }  
}  

Activity里没什么了,就是注册监听,开始下载,取消下载这些了,下面我们来看看效果吧

运行效果

代码

http://download.csdn.net/detail/cfy137000/9746583

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/28618.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

常见的宽基指数基金

指数基金投资指南 ❝ 这篇博客里面的内容主要来自于银行螺丝钉的《定投十年&#xff0c;财务自由》和《指数基金投资指南》这两本书中章“常见的宽基指数”&#xff0c;最近第三次读这本书&#xff0c;打算做一点笔记加深自己的印象。 博客中很多内容是从书中摘抄的&#xff0c…

【git使用三】git工作机制与命令用法

目录 git工作机制和相关概念 四个重要区域 分支的概念 上传代码到远程分支的基本流程 克隆代码 仓库同步 开发者如何提交代码到远程仓库分支 1.初始化本地仓库 2.关联本地仓库和远程仓库 创建关联 查看关联情况 如何解除关联 3.推送代码到远程仓库 3.1先下拉远程…

PyTorch计算机视觉入门:测试模型与评估,对单帧图片进行推理

在完成模型的训练之后&#xff0c;对模型进行测试与评估是至关重要的一步&#xff0c;它能帮助我们理解模型在未知数据上的泛化能力。本篇指南将带您了解如何使用PyTorch进行模型测试&#xff0c;并对测试结果进行分析。我们将基于之前训练好的模型&#xff0c;演示如何加载数据…

解决老毛子路由器自带微信提示功能无法触发问题

新买了一个二手的RM AC2100&#xff0c;刷了老毛子后&#xff0c;发现自带的上下线微信提示无法使用(方糖公众号无信息) 经我开启SSH&#xff0c;将上下线部分代码拿出来调试发现&#xff0c;发不出来的原因是原版信息发送长度过长&#xff0c;需要截取一部分才能发送成功。 …

【Android面试八股文】说一说JVM、DVM(Dalvik VM)和ART的区别

文章目录 1. JVM(Java Virtual Machine):2. DVM(Dalvik Virtual Machine):与JVM区别基于的架构不同执行的字节码不同3. ART(Android Runtime):与DVM的区别4. 什么是JIT?4.1 什么是JIT4.2 JIT 编译的优势包括:4.3 对于 DVM 和 ART,它们在 JIT(Just-In-Time)编译方…

【学习笔记】centos7安装mysql相关事项

究极恶心的体验 依赖要按照顺序安装&#xff0c;有些依赖安装位置也不同 非常细节 mysql安装包&#xff1a;mysql官网下载地址 centos7选择Red Hat Enterprise Linux 7 / Oracle Linux 7 (x86, 64-bit), RPM Bundle 下载版本自选 安装视频教程&#xff1a;centos7.5安装mysql …

消息队列-RabbitMQ-延时队列实现

死信队列 DLX,全称为Dead-Letter-Exchange,死信交换机&#xff0c;死信邮箱。当消息在一个队列中变成死信之后&#xff0c;它能重新发送到另外一个交换器中&#xff0c;这个交换器就是DLX&#xff0c;绑定DLX的队列就称为死信队列。 导致死信的几种原因&#xff1a; ● 消息…

Spring配置那些事

一、引言 配置是一个项目中不那么起眼&#xff0c;但却有非常重要的东西。在工程项目中&#xff0c;我们一般会将可修改、易变、不确定的值作为配置项&#xff0c;在配置文件/配置中心中设置。 比方说&#xff0c;不同环境有不同的数据库地址、不同的线程池大小等&#xff0c…

【计算机毕业设计】241外卖微信小程序

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

jadx+android studio+雷电模拟器 动态调试apk

# 环境准备 1.雷电模拟器&#xff0c;开启root 2.jadx&#xff1a; https://sourceforge.net/projects/jadx.mirror/files/v1.5.0/jadx-gui-1.5.0-with-jre-win.zip/download 3.java jdk 11 https://www.oracle.com/cn/java/technologies/javase/jdk11-archive-downloads.…

【文献阅读】Adaptive Arrays

符号表 由于论文内符号繁杂&#xff0c;这里写了一个符号表 符号含义 μ k l \mu_{kl} μkl​kl协方差项 n k n_k nk​K通道的复包络 n l n_l nl​L通道的复包络 μ l k \mu_{lk} μlk​kl协方差项的共轭 α \alpha α定义信号的幅度和时间变化 s k s_k sk​k信道中的信号 w …

什么是Dubbo?

文章目录 1、Dubbo介绍1.1 什么是Dubbo1.2 Dubbo关键特性1.3 什么是RPC1.4 实现RPC的方式1.5 Dubbo中的五大核心组件 1、Dubbo介绍 Apache Dubbo是一款高性能、轻量级的开源微服务开发框架&#xff0c;它提供了RPC通信与微服务治理两大关键能力。这意味着&#xff0c;使用Dubbo…

【Linux】进程_6

文章目录 五、进程8. 进程地址空间 未完待续 五、进程 8. 进程地址空间 上图可能很多人都看过了&#xff0c;这里再来验证一下&#xff1a; 验证位置&#xff1a; 验证堆栈的生长方向&#xff1a; 在上面的空间布局图中&#xff0c;有一个疑问&#xff0c;画的空间是 内存…

【RabbitMQ】初识 RabbitMQ

初识 RabbitMQ 1.认识 RabbitMQ1.1 介绍1. 2.使用场景1.2.1 推送通知1.2.2 异步任务1.2.3 多平台应用的通信1.2.4 消息延迟1.2.5 远程过程调用 1.3 特性 2.基本概念2.1 生产者、消费者和代理2.2 消息队列2.3 交换机2.3.1 direct2.3.2 topic2.3.3 headers2.3.4 fanout 2.4 绑定2…

docker hub仓库被禁用,镜像加速器站点替换

整理 站点整理之前用的daemon.json,现更改镜像加速地址替换自己的docker加速器daemon.json前面加https:// 站点整理 之前用的daemon.json,现更改镜像加速地址 vim /etc/docker/daemon.json{"registry-mirrors": ["https://4xgbe4ey.mirror.aliyuncs.com",…

计算缺失msvcr120.dll文件怎么办,msvcr120.dll丢失的解决方法分享

在计算机使用过程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是“找不到msvcr120.dll”。那么&#xff0c;msvcr120.dll到底是什么&#xff1f;为什么计算机会找不到它&#xff1f;它会对计算机产生什么具体影响&#xff1f;如何解决这个问题&#xff1f;…

vue框架学习------框架概述

框架 在基础语言之上,对各种基础功能进行封装 什么是框架 框架&#xff08;Framework&#xff09;是整个或部分系统的可重用设计&#xff0c;表现为一组抽象构件及构件实例间交互的方法; 另一种定义认为&#xff0c;框架是可被应用开发者定制的应用骨架。前者是从应用方面而…

正能量情感语录热门素材文案去哪里找?文案素材网站分享

正能量情感语录热门素材文案去哪里找&#xff1f;文案素材网站分享 想为你的作品注入正能量和情感温度&#xff1f;不知如何获取热门情感语录素材&#xff1f;别担心&#xff0c;今天我将为大家推荐一些海外知名的素材网站&#xff0c;让你轻松找到受欢迎的文案素材&#xff…

ffmpeg解封装rtsp并录制视频-(1)解封装rtsp断网或摄像机重启后自动重连处理

头文件&#xff1a; xtools.h #pragma once #include <thread> #include <iostream> #include <mutex> //日志级别 DEBUG INFO ERROR FATAL enum XLogLevel {XLOG_TYPE_DEBUG,XLOG_TYPE_INFO,XLOG_TPYE_ERROR,XLOG_TYPE_FATAL }; #define LOG_MIN_LEVEL XLO…

dp练习题

先来一个简单dp练习 class Solution { public:int rob(vector<int>& nums) {int n nums.size();vector<int> a(n 1);int ans nums[0]; a[0] nums[0];if (n 1) return ans;a[1] max(nums[0], nums[1]);ans max(ans, a[1]);if (n 2) return ans;for (i…