之前博客《 Android学习笔记之——Service 》和《Android 学习笔记之——service进一步探索 》已经详细介绍了服务的各种用法。本博文尝试实现一个在服务中经常会使用到的功能——下载
首先创建一个ServiceBestPractice项目。
首先我们需要将项目中会使用到的依赖库添加好,编辑app/build.gradle文件,在dependencies闭包中添加如下内容:
额外新增的是
implementation 'com.squareup.okhttp3:okhttp:3.4.1'
接下来需要定义一个回调接口,用于对下载过程中的各种状态进行监听和回调。新建一个DownloadListener 接口,代码如下所示:
package com.example.servicebestpractice;
public interface DownloadListener {
//在接口中定义一系列抽象函数
void onProgress(int progress);//法用于通知当前的下载进度
void onSuccess();//用于通知下载成功事件
void onFailed();//法用于通知下载失败事件
void onPaused();//用于通知下载暂停事件
void onCanceled();//用于通知下载取消事件
}
回调接口定义好之后,开始编写下载功能。采用之前博客《 Android学习笔记之——Service 》介绍过的AsyncTask来进行实现
package com.example.servicebestpractice;
import android.os.AsyncTask;
import android.os.Environment;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class DownloadTask extends AsyncTask<String, Integer, Integer> {
//首先在AsyncTask中的三个泛型参数
//第一个泛型参数指定为String ,表示在执行AsyncTask的时候需要传入一个字符串参数给后台任务
//第二个泛型参数指定为Integer ,表示使用整型数据来作为进度显示单位
//第二个泛型参数指定为Integer ,表示使用整型数据来作为进度显示单位
//接下来我们定义了4个整型常量用于表示下载的状态
public static final int TYPE_SUCCESS = 0;//下载成功
public static final int TYPE_FAILED = 1;//下载失败
public static final int TYPE_PAUSED = 2;//暂停下载
public static final int TYPE_CANCELED = 3;//取消下载
//定义一系列参数
private DownloadListener listener;
private boolean isCanceled = false;
private boolean isPaused = false;
private int lastProgress;
//然后在DownloadTask 的构造函数中要求传入一个刚刚定义的DownloadListener 参数
// 我们待会就会将下载的状态通过这个参数进行回调。
public DownloadTask (DownloadListener listener){
this.listener=listener;
}
//这个方法中的所有代码都会在子线程中运行,
// 我们应该在这里去处理所有的耗时任务。
// 任务一旦完成就可以通过return 语句来将任务的执行结果返回,
// 如果AsyncTask的第三个泛型参数指定的是Void ,就可以不返回任务执行结果。(但是此处是Integer)
@Override
protected Integer doInBackground(String... params) {//用于在后台执行具体的下载逻辑
InputStream is = null;
RandomAccessFile savedFile = null;
File file = null;
try {
long downloadedLength = 0; // 记录已下载的文件长度
String downloadUrl = params[0];//从参数中获取到下载的URL地址
//指定将文件下载到Environment.DIRECTORY_DOWNLOADS目录下,也就是SD卡的Download目录。
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));//根据URL地址解析出了下载的文件名。
String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();//要下载的目录名
file = new File(directory + fileName);
//判断一下Download目录中是不是已经存在要下载的文件了
if (file.exists()) {//如果已经存在的话则读取已下载的字节数,这样就可以在后面启用断点续传的功能
downloadedLength = file.length();
}
//调用了getContentLength() 方法来获取待下载文件的总长度
long contentLength = getContentLength(downloadUrl);
if (contentLength == 0) {//如果文件长度等于0则说明文件有问题
return TYPE_FAILED;
} else if (contentLength == downloadedLength) {
// 已下载字节和文件总字节相等,说明已经下载完成了
return TYPE_SUCCESS;
}
//紧接着使用OkHttp来发送一条网络请求
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
// 断点下载,指定从哪个字节开始下载(已下载过的部分就不需要下载了)
.addHeader("RANGE", "bytes=" + downloadedLength + "-")//在请求中添加了一个header,用于告诉服务器我们想要从哪个字节开始下载
.url(downloadUrl)
.build();
//接下来读取服务器响应的数据。,并使用Java的文件流方式,
// 不断从网络上读取数据,不断写入到本地,一直到文件全部下载完成为止。
Response response = client.newCall(request).execute();
if (response != null) {
is = response.body().byteStream();
savedFile = new RandomAccessFile(file, "rw");
savedFile.seek(downloadedLength); // 跳过已下载的字节
byte[] b = new byte[1024];
int total = 0;
int len;
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);
//调用publishProgress() 方法进行通知
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 (Exception e) {
e.printStackTrace();
}
}
return TYPE_FAILED;
}
//定义getContentLength() 方法来获取待下载文件的总长度
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.body().close();
return contentLength;
}
return 0;
}
//用于在界面上更新当前的下载进度
@Override
protected void onProgressUpdate(Integer... values) {
int progress = values[0];//首先从参数中获取到当前的下载进度
if (progress > lastProgress) {//和上一次的下载进度进行对比
//如果有变化的话则调用DownloadListener的onProgress() 方法来通知下载进度更新。
listener.onProgress(progress);
lastProgress = progress;
}
}
//用于通知最终的下载结果
@Override
protected void onPostExecute(Integer status) {//根据参数中传入的下载状态来进行回调
switch (status) {//对应不同状态就调用不同的方法
case TYPE_SUCCESS:
listener.onSuccess();
break;
case TYPE_FAILED:
listener.onFailed();
break;
case TYPE_PAUSED:
listener.onPaused();
break;
case TYPE_CANCELED:
listener.onCanceled();
break;
default:
break;
}
}
//暂停和取消操作都是使用一个布尔型的变量来进行控制的,
// 调用pauseDownload()或cancelDownload() 方法即可更改变量的值。
public void pauseDownload() {
isPaused = true;
}
public void cancelDownload() {
isCanceled = true;
}
}
把具体的下载功能完成了之后,为了保证DownloadTask可以一直在后台运行,我们还需要创建一个下载的服务。右击com.example.servicebestpractice→New→Service→Service,新建DownloadService,然后修改其中的代码,如下所示:
package com.example.servicebestpractice;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.os.Binder;
import android.os.Environment;
import android.os.IBinder;
import android.widget.Toast;
import androidx.core.app.NotificationCompat;
import java.io.File;
public class DownloadService extends Service {
private DownloadTask downloadTask;
private String downloadUrl;
//首先这里创建了一个DownloadListener的匿名类实例,
//并在匿名类中实现了5个方法
private DownloadListener listener = new DownloadListener() {
@Override
public void onProgress(int progress) {
//调用getNotification() 方法构建了一个用于显示下载进度的通知
//该方法返回NotificationManager类型的数据
//然后调用NotificationManager的notify() 方法去触发这个通知,
getNotificationManager().notify(1, getNotification("Downloading...", progress));
}
@Override
public void onSuccess() {
downloadTask = null;
// 下载成功时将前台服务通知关闭,并创建一个下载成功的通知
stopForeground(true);//将正在下载的前台通知关闭
//然后创建一个新的通知用于告诉用户下载成功了
getNotificationManager().notify(1, getNotification("Download Success",-1));
Toast.makeText(DownloadService.this, "Download Success",Toast.LENGTH_SHORT).show();
}
//类似于onSuccess(),这里是告诉用户下载失败
@Override
public void onFailed() {
downloadTask = null;
// 下载失败时将前台服务通知关闭,并创建一个下载失败的通知
stopForeground(true);
getNotificationManager().notify(1, getNotification("Download Failed",-1));
Toast.makeText(DownloadService.this, "Download Failed", Toast.LENGTH_SHORT).show();
}
//类似于onSuccess(),这里是告诉用户下载暂停
@Override
public void onPaused() {
downloadTask = null;
Toast.makeText(DownloadService.this, "Paused", Toast.LENGTH_SHORT).show();
}
//类似于onSuccess(),这里是告诉用户下载取消
@Override
public void onCanceled() {
downloadTask = null;
stopForeground(true);
Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show();
}
};
//为了让DownloadService可以和活动进行通信。
//创建了一个DownloadBinder
private DownloadBinder mBinder = new DownloadBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
class DownloadBinder extends Binder {//内部类
//定义开始下载
public void startDownload(String url) {
if (downloadTask == null) {
downloadUrl = url;//下载文件的URL地址
downloadTask = new DownloadTask(listener);
downloadTask.execute(downloadUrl);//调用execute() 方法开启下载
//为了让这个下载成为一个前台服务。调用了startForeground() 方法
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();
}
}
}
}
//定义一个函数getNotification() ,用于显示下载进度的通知
private NotificationManager getNotificationManager() {
return (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
}
//DownloadService 类中所有使用到的通知都是调用getNotification() 方法进行构建的
private Notification getNotification(String title, int progress) {
Intent intent = new Intent(this, MainActivity.class);
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) {
// 当progress大于或等于0时才需显示下载进度
builder.setContentText(progress + "%");
builder.setProgress(100, progress, false);
}
return builder.build();
}
}
接下来修改activity_main.xml中的代码,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<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>
然后修改MainActivity中的代码,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.servicebestpractice">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
android:usesCleartextTraffic="true"
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".DownloadService"
android:enabled="true"
android:exported="true" />
</application>
</manifest>
运行结果
这是由于Android9.0对未加密的流量不在信任,添加了新的限制。
在Android 的mainfest.xml中的application添加一句配置
android:usesCleartextTraffic="true"
但是仍然是运行不了。。。看来就是之前遇到的前台服务没有解决而遗留下来的问题了。。。。