看了第一行代码,里面使用AsyncTask异步请求、OkHttp网络连接以及Service服务实现了一个从指定网站下载文件。
整个过程大致说一下:
1.首先创建一个布局以及Activity来启动服务进行开始下载,暂停下载,取消下载的服务。
2.在服务中连接一个异步请求,在异步请求中下载文件,而且在服务中创建一个Notification通知,提示使用者目前的下载状态。
3.在异步请求中进行下载文件的操作,通过OkHttp得到文件总长度,然后再从OkHttp中得到response,在response中得到输入流,进而通过存储到本地的文件长度和文件总长度的比例得到下载进度。
首先实现一个借口封装一个下载接收器:
public interface DownloadListener {
//通知当前的下载进度
void onProgress(int progress);
//通知下载成功事件
void onSuccess();
//通知下载失败事件
void onFailed();
//通知下载暂停事件
void onPaused();
//通知下载取消事件
void onCanceled();
}
之后实现一个下载的异步请求:
public class DownloadTask extends AsyncTask<String, Integer, Integer> {
private static final int TYPE_SUCCESS = 0;
private static final int TYPE_FAILED = 1;
private static final int TYPE_PAUSED = 2;
private static final int TYPE_CANCELED = 3;
private DownloadListener listener;
private boolean isCanceled = false;
private boolean isPaused = false;
private int lastProgress;
public DownloadTask(DownloadListener listener){
this.listener = listener;
}
//params:在执行AsyncTask时需要传入的参数,在整个后台任务中使用。
//progres:后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。
//result:任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为进度单位。
//这个方法会在后台任务开始执行前调用,用于进行一些界面上的初始化操作。
// @Override
// protected void onPreExecute() {
// super.onPreExecute();
// }
//在该方法中的所有代码都会在子线程中运行,也就是说所有耗时任务都会在这里实现。
//params:在执行AsyncTask时需要传入的参数,在整个后台任务中使用。(可变参数)
@Override
protected Integer doInBackground(String... params) {
// 输入流
InputStream is = null;
// RandomAccessFile类的主要功能是完成随机读取功能,可以读取指定位置的内容。
// 之前的File类只是针对文件本身进行操作的,而如果要想对文件内容进行操作,则可以使用RandomAccessFile类
// 此类属于随机读取类,可以随机读取一个文件中指定位置的数据
RandomAccessFile savedFile = null;
// 文件
File file = null;
try{
long downloadedLength = 0;
//传入可变参数的第一个参数为Url
String downloadUrl = params[0];
//public String substring(int beginIndex)
//第一个int为开始的索引,对应String数字中的开始位置,到String最后
//lastIndexOf 是定位到最后一个"/"的位置
//这里得到的就是文件名字
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
//File sdCard = Environment.getExternalStorageDirectory();
//这个sdCard的路径为mnt/sdcard/ 即为SD卡根路径,我们可以指定访问的文件夹名
//File directory_pictures = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
//第二种方法是一个更加方便的访问Android给我们提供好的一些公共目录的方法,第一种方式更加灵活,可以自己指定目录。
//这里是获取公共目录Downloads的路径
String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
//通过得到的directory以及filename创建文件
file = new File(directory + fileName);
//得到文件的长度
if(file.exists()){
downloadedLength = file.length();
}
//从网上(使用OkHttp)得到文件的长度(通过Response中的长度)这个是完整的文件长度,downloadedLength为下载的长度
long contentLength = getContentLength(downloadUrl);
if(contentLength == 0){
return TYPE_FAILED;
}else if(contentLength == downloadedLength){
return TYPE_SUCCESS;
}
OkHttpClient client = new OkHttpClient();
//添加头部
Request request = new Request.Builder()
.addHeader("RANGE", "bytes=" + downloadedLength + "-")
.url(downloadUrl)
.build();
Response response = client.newCall(request).execute();
if(response != null){
//输入流得到response中的byteStream
is = response.body().byteStream();
//使用RandomAccessFile来读写文件
savedFile = new RandomAccessFile(file, "rw");
//从已经下载到的那个字节开始读取文件
savedFile.seek(downloadedLength);
byte[] b = new byte[1024];
int total = 0;
int len;
//读InputStream中的byte[]
while((len = is.read(b)) != -1){
//这里在循环中,每次都询问一次,这样如果实时过来暂停和取消信息,可以直接暂停和取消。
if(isCanceled){
return TYPE_CANCELED;
}else if(isPaused){
return TYPE_PAUSED;
}else{
total += len;
//继续存入文件
savedFile.write(b, 0, len);
int progress = (int) ((total + downloadedLength) * 100 / contentLength);
//转入onProgressUpdate
publishProgress(progress);
}
}
response.body().close();
return TYPE_SUCCESS;
}
}catch (Exception e){
e.printStackTrace();
}finally {
//把输入流、文件等东西全部关闭
try{
if(is != null){
is.close();
}
if(savedFile != null){
savedFile.close();
}
if(isCanceled && file != null){
file.delete();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return TYPE_FAILED;
}
//当在后台任务中调用了publishProgress(Progress...)方法后,onProgressUpdate(Progress)方法就会很快被调用
//该方法中携带的参数就是后台任务中传递过来的,在这个方法中可以利用参数中的数值对界面元素进行UI更新操作。
@Override
protected void onProgressUpdate(Integer... values) {
int progress = values[0];
if(progress < lastProgress){
listener.onProgress(progress);
lastProgress = progress;
}
}
//当后台任务执行完毕并通过return语句发挥的时候,这个方法会被调用。
//返回的数据作为参数传递到此方法中,可以使用返回数据进行UI操作。
@Override
protected void onPostExecute(Integer staus) {
switch (staus){
case TYPE_SUCCESS:
listener.onSuccess();
break;
case TYPE_FAILED:
listener.onFailed();
break;
case TYPE_PAUSED:
listener.onPaused();
break;
case TYPE_CANCELED:
listener.onCanceled();
default:
break;
}
}
public void pauseDownload(){
isPaused = true;
}
public void cancelDownload(){
isCanceled = true;
}
//通过下载的Url来进行OkHttp连接,得到Response之后,分析Response的长度,返回文件的长度。
private long getContentLength(String downloadUrl) throws IOException {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(downloadUrl)
.build();
Response response = client.newCall(request).execute();
if(response != null && response.isSuccessful()){
long contentLength = response.body().contentLength();
//关闭response
response.body().close();
return contentLength;
}
return 0;
}
}
接着是布局文件以及AndroidManifest中的配置:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/start_download"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="start download"/>
<Button
android:id="@+id/pause_download"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="pause download"/>
<Button
android:id="@+id/cancel_download"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="cancel download"/>
</LinearLayout>
<service android:name=".tools.DownloadService"
android:enabled="true"
android:exported="true"/>
实现一个服务:
public class DownloadService extends Service {
private DownloadTask downloadTask;
private String downloadUrl;
private DownloadListener listener = new DownloadListener() {
//Foreground Service:前台服务,因为Service实际上始终是工作在后台的。由于Service工作在后台的原因,
//使用者并不知道它在运行, 有时候开发者需要使用者知道某个Service在运行时,就需要Foreground Service。
//简单来说就是使用Notification通知来告知用户Service正在运行。
@Override
public void onProgress(int progress) {
getNotificationManager().notify(1, getNotification("Downloading...", progress));
}
//下载成功过后关闭前台服务,显示提示。
@Override
public void onSuccess() {
downloadTask = null;
stopForeground(true);
getNotificationManager().notify(1, getNotification("Downloading Success", -1));
Toast.makeText(DownloadService.this, "Download Success", Toast.LENGTH_SHORT).show();
}
//下载失败过后关闭前台服务,显示提示。
@Override
public void onFailed() {
downloadTask = null;
stopForeground(true);
getNotificationManager().notify(1, getNotification("Downloading Failed", -1));
Toast.makeText(DownloadService.this, "Download Failed", Toast.LENGTH_SHORT).show();
}
//下载暂停过后显示提示。
@Override
public void onPaused() {
downloadTask = null;
Toast.makeText(DownloadService.this, "Paused", Toast.LENGTH_SHORT).show();
}
//下载取消过后关闭前台服务,显示提示。
@Override
public void onCanceled() {
downloadTask = null;
stopForeground(true);
Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show();
}
};
//这里设定了一种Binder来使Activity与Service进行绑定
private DownloadBinder mBinder = new DownloadBinder();
//在Bind的过程中bind的是DownloadBinder这一种Binder
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
public class DownloadBinder extends Binder{
//这里写了一个开始进行下载的方法,其中得到Url以及配置异步请求
public void startDownload(String url){
if(downloadTask == null){
downloadUrl = url;
downloadTask = new DownloadTask(listener);
//开始执行异步请求
downloadTask.execute(downloadUrl);
//开启前台服务
startForeground(1, getNotification("Downloading...", 0));
Toast.makeText(DownloadService.this, "Downloading...", Toast.LENGTH_SHORT).show();
}
}
public void pauseDownload(){
//如果下载正在处理,那么将其暂停
if(downloadTask != null){
downloadTask.pauseDownload();
}
}
public void cancelDownload(){
//如果正在进行下载请求,则取消
if(downloadTask != null){
downloadTask.cancelDownload();
}else {
//如果没有正在进行下载请求,则删除文件,停止前台服务
if(downloadUrl != null){
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
File file = new File(directory + fileName);
if(file.exists()){
file.delete();
}
getNotificationManager().cancel(1);
stopForeground(true);
Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show();
}
}
}
}
//获取系统服务通知服务
private NotificationManager getNotificationManager(){
return (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
}
//这里是创建一个Notification通知来进行提示。
private Notification getNotification(String title, int progress){
//指出点击这个通知进入哪个界面
Intent intent = new Intent(this, DownloadActivity.class);
//供当前App或之外的其他App调用,而常见的是供外部App使用,外部App执行这个 PendingIntent时,间接地调用里面的Intent
//即外部App延时执行PendingIntent中描述的Intent及其最终行为
//即使当前App已经不存在了,也能通过存在于PendingIntent里的 Context来执行Intent。
PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
builder.setContentIntent(pi);
builder.setContentTitle(title);
if(progress > 0){
builder.setContentText(progress + "%");
builder.setProgress(100, progress, false);
}
return builder.build();
}
}
实现一个Activity进行开启服务,实现下载的入口:
public class DownloadActivity extends Activity implements View.OnClickListener{
private DownloadService.DownloadBinder downloadBinder;
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//把设置好的service与DownloadBinder绑定。
downloadBinder = (DownloadService.DownloadBinder) service;
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.download_activity_layout);
Button startDownload = findViewById(R.id.start_download);
Button pauseDownload = findViewById(R.id.pause_download);
Button cancelDownload = findViewById(R.id.cancel_download);
startDownload.setOnClickListener(this);
pauseDownload.setOnClickListener(this);
cancelDownload.setOnClickListener(this);
//开启服务
Intent intent = new Intent(this, DownloadService.class);
startService(intent);
bindService(intent, connection, BIND_AUTO_CREATE);
if(ContextCompat.checkSelfPermission(DownloadActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(DownloadActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1);
}//添加权限(显示添加权限的界面)
}
@Override
public void onClick(View v) {
if(downloadBinder == null){
return;
}
switch (v.getId()){
case R.id.start_download:
String url = "https://mirrors.tuna.tsinghua.edu.cn/apache/accumulo/1.7.4/accumulo-1.7.4-bin.tar.gz";
downloadBinder.startDownload(url);
break;
case R.id.pause_download:
downloadBinder.pauseDownload();
break;
case R.id.cancel_download:
downloadBinder.cancelDownload();
break;
default:
break;
}
}
//实现权限管理
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode){
case 1:
if(grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED){
Toast.makeText(DownloadActivity.this, "拒绝权限将无法使用程序",Toast.LENGTH_SHORT).show();
finish();
}
break;
default:
break;
}
}
//退出时unBind
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(connection);
}
}
一个开始下载的流程(个人理解),整个过程就是一个 Activity <--> Service <--> AsyncTask:
1.首先在Activity中启动服务,判断好是否有读写权限,之后点击开始下载的按钮,将url传入。
2.接着转入Service当中,在Service中配置好,之后开始执行异步请求,然后向用户发送一个请求正在进行的信息。
3.接着转入异步请求当中,在异步请求中,我们首先初始化好输入流,然后定义好文件存储的路径。这时候有两个操作:第一个操作是,从OkHttp的Response中获取文件的大小(长度),这就是我们需要下载的文件大小(长度)。第二个操作是,判断本地是否存在一个相同名字的文件,如果不存在则创建一个文件,开始传输数据,如果存在,那么就找到他的最后一个字节的位置,然后定位,从这个位置下载文件(也就是断点续传?)。
4.在异步请求中,正在下载文件的过程中,每获得一份数据(暂且这么称呼,不断循环的获取数据最后得到所有数据),系统都会判断一次是否得到一个暂停或者取消的指令(这个指令是通过Activity给Service传入,然后Service传入AsyncTask),如果有这些指令,直接暂停或取消,跳出doInBackground()方法。
5.在每获得一次数据中,如果成功的获取到了这一份数据,那么在这次循环的最后会调用publishProgress(progress);方法,这个方法可以转入onProgressUpdate()方法中,用来得到目前的进度,可以通过进度实时的更改UI,反馈给用户正在下载的进度。