在前面的学习中,我们已经掌握了Service的用法,今天尝试实现一个在服务中经常会用到的功能 -- 下载。
在这个demo中,将涉及到许多前面的知识,大伙儿准备好了么?首先我们需要添加待会儿需要用到的依赖库
dependencies {
...
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
}
这里只添加了一个OKHttp的依赖,用于网络功能的部分,我们将使用OKHttp实现。
接下来定一个回调接口,用于对下载过程中的各种状态进行监听和回调。新建DownloadListener接口,代码如下:
public interface DownloadListener {
void onProgress(int progress);
void onSuccess();
void onFailed();
void onPaused();
void onCancled();
}
这里我们简单的定义了5个回调方法,onProgress()用于通知下载进度,onSuccess()用于通知下载成功事件,onFailed()用于通知下载失败事件,onPaused()用于通知暂停下载事件,onCancled()用于通知取消下载事件。
回调接口定义好后,就可以开始编写下载功能了,这里我们用刚刚学过的AsyncTask来实现,新建DownloadTask并继承AsyncTask:
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_CANCLED = 3;
private DownloadListener listener;
private boolean isCancled = false;
private boolean isPaused = false;
private int lastProgress;
public DownloadTask(DownloadListener listener) {
this.listener = listener;
}
@Override
protected Integer doInBackground(String... params) {
InputStream in = null;
RandomAccessFile savedFile = null;
File file = null;
try {
long downloadedLength = 0; // 记录已下载的文件长度
String downloadUrl = params[0];
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
file = new File(directory + fileName);
if (file.exists()) {
downloadedLength = file.length();
}
long contentLength = 0;
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()
.url(downloadUrl)
.build();
Response response = client.newCall(request).execute();
if (response != null) {
in = response.body().byteStream();
savedFile = new RandomAccessFile(file, "rw");
savedFile.seek(downloadedLength);
byte[] b = new byte[1024];
int total = 0;
int len;
while ((len = in.read(b)) != -1) {
if (isCancled) {
return TYPE_CANCLED;
} else if (isPaused) {
return TYPE_PAUSED;
} else {
total += len;
savedFile.write(b, 0, len);
int progress = (int) ((total + downloadedLength) * 100 / contentLength);
publishProgress(progress);
}
}
response.body().close();
return TYPE_SUCCESS;
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (in != null) {
in.close();
}
if (savedFile != null) {
savedFile.close();
}
if (isCancled && file != null) {
file.delete();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return TYPE_FAILED;
}
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.close();
return contentLength;
}
return 0;
}
@Override
protected void onProgressUpdate(Integer... values) {
int progress = values[0];
if (progress > lastProgress) {
listener.onProgress(progress);
lastProgress = progress;
}
}
@Override
protected void onPostExecute(Integer status) {
switch (status) {
case TYPE_SUCCESS:
listener.onSuccess();
break;
case TYPE_CANCLED:
listener.onCancled();
break;
case TYPE_PAUSED:
listener.onPaused();
break;
case TYPE_FAILED:
listener.onFailed();
break;
default:
break;
}
}
public void pauseDownload() {
isPaused = true;
}
public void cancleDownload() {
isCancled = true;
}
}
代码比之前的练习稍微长了少许,一点点来分析。先来看AsyncTask的3个泛型参数,这里我们把第一个泛型参数指定为String类型,表示在AsyncTask时需要传一个String类型的参数给后台任务;第二个泛型指定为Integer,表是用整型数据作为进度的显示单位;第三个泛型指定为Integer,表示将使用整型数据来反馈执行结果。
接下来我们定义了4个整型常量来表示下载的状态,TYPE_SUCCESS表示下载成功、TYPE_FAILED表示下载失败、TYPE_PAUSED表示下载暂停,TYPE_CANCLED表示下载取消。然后在DownloadTask的构造函数中要求传入我们刚才创建的DownloadListener参数,我们待会就会将下载的状态通过这个参数进行回调。
接下来就是重写doInBackground()、onProgressUpdate、onPostExecute方法了。
先来看doInBackground()方法,我们从参数中获得到了下载的URL,并根据URL解析出下载的文件名,然后指定下载目录Environment.DIRECTORY_DOWNLOADS,也就是/sdcard/Downlaod/文件夹。然后我们判断一下本地是否已经存在下载文件,如果存在就对比一下本地文件和远端文件的大小,如果大小一致,则不用从新下载,直接返回TYPE_SUCCESS状态。如果获取不到远端文件的大小,则认为下载失败,直接返回TYPE_FAILED状态。
如果本地文件不存在,且能够获取到远端的文件大小,则开始使用OkHttp来发送网络请求,然后开始写入数据。在这个过程中,我们还得判断用户是否有暂停和取消下载的动作,如果有则返回对应的状态,如TYPE_CANCLED或者TYPE_PAUSED。暂停和取消的状态,我们都是使用布尔型来进行控制的。最后直到完成数据的写入,返回TYPE_SUCCESS状态。这里由于涉及到读写,因此需要捕获异常,并在finally中关闭资源。
onProgressUpdate()方法就比较简单了,首先从参数中获取到传来的下载进度,然后和上一次的值做对比,如果有变化,就调用DownloadListener的onProgress()方法来通知下载进度跟新。
onPostExecute()同样很简单,根据后台任务传入的下载状态来进行回调,分别调用DownloadListener的各个对应方法。
这样下载功能就算基本完成了,我们接下来还需要一个下载的Service来保证DownloadTask一直在后台中执行,新建一个Service -- DownloadService:
public class DownloadService extends Service {
private DownloadTask downloadTask;
private String downloadUrl;
private DownloadListener listener = new DownloadListener() {
@Override
public void onProgress(int progress) {
getNotificationManager().notify(1, getNotification("Download...", 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();
}
@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();
}
@Override
public void onPaused() {
downloadTask = null;
Toast.makeText(DownloadService.this, "Download Paused", Toast.LENGTH_SHORT).show();
}
@Override
public void onCancled() {
downloadTask = null;
stopForeground(true);
Toast.makeText(DownloadService.this, "Download Cancled", Toast.LENGTH_SHORT).show();
}
};
class DownloadBinder extends Binder {
public void startDownload(String url) {
if (downloadTask == null) {
downloadUrl = url;
downloadTask = new DownloadTask(listener);
// downloadTask.execute(downloadUrl);
downloadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, downloadUrl);
startForeground(1, getNotification("Downloading...", 0));
Toast.makeText(DownloadService.this, "Downloading...", Toast.LENGTH_SHORT).show();
}
}
public void pausedDownload() {
if (downloadTask != null) {
downloadTask.pauseDownload();
}
}
public void cancelDownload() {
if (downloadTask != null) {
downloadTask.cancleDownload();
} else {
if (downloadUrl != null) {
String fielname = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
File file = new File(directory + fielname);
if (file.exists()) {
file.delete();
}
getNotificationManager().cancel(1);
stopForeground(true);
Toast.makeText(DownloadService.this, "Cancled", Toast.LENGTH_SHORT).show();
}
}
}
}
private IBinder mBinder = new DownloadBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
private NotificationManager getNotificationManager() {
return (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
}
private Notification getNotification(String title, int progress) {
Intent intent = new Intent(this, DownloadPracticeActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent,0);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "default");
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setContentIntent(pendingIntent);
builder.setContentTitle(title);
if (progress > 0) {
builder.setContentText(progress + "%s");
builder.setProgress(100, progress, false);
}
return builder.build();
}
}
我们首先创建了DownloadListener的匿名类实例,并实现了onProgress()、onSuccess()、onFailed()、onPaused()和onCancled()5个方法。在onProgress()方法中,我们通过调用getNotificationManager()方法构建了一个用于显示下载进度的通知,然后调用NotificationManager中的notify方法去触发这个通知。这样在通知栏下拉就能看到当前的下载进度了。其他几个方法类似,都是构造一个通知来告诉用户下载的状态。
接下来为了让DownloadService可以和Activity进行通信,我们创建了一个DownloadBinder。DownloadBinder中提供了startDownload()、pausedDownload()、cancelDownload()3个方法。startDownload()方法中,我们创建了DownloadTask的实例,然后把刚才的DownloadListener最为参数传进去,然后调用executeOnExecutor开启下载。同时为让这个下载成为一个前台服务,我们还调用了startForeground()方法,这样就在系统的状态栏创建了一个持续存在通知。pausedDownload()很简单,只是调用了DownloadTask的pauseDownload()方法。cancelDownload()也类似,只是多了一步删除文件的操作。
Service完成后,我们就可以开始编写主Activity了,在布局中添加3个按钮,分别为开始下载,暂停下载和取消下载。
public class DownloadPracticeActivity extends BaseActivity implements View.OnClickListener{
private DownloadService.DownloadBinder downloadBinder;
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
downloadBinder = (DownloadService.DownloadBinder) service;
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_download_practice);
setTitle("Download Practice");
Button startDownload = findViewById(R.id.btn_strat_download);
Button pauseDownlaod = findViewById(R.id.btn_pause_download);
Button cancleDownlaod = findViewById(R.id.btn_cancle_download);
startDownload.setOnClickListener(this);
pauseDownlaod.setOnClickListener(this);
cancleDownlaod.setOnClickListener(this);
Intent intent = new Intent(this, DownloadService.class);
startService(intent);
bindService(intent, connection, BIND_AUTO_CREATE);
if (ContextCompat.checkSelfPermission(DownloadPracticeActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(DownloadPracticeActivity.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.btn_strat_download:
String url = "http://img01.oneniceapp.com/app/nice-main-4.8.6-release.apk";
downloadBinder.startDownload(url);
break;
case R.id.btn_pause_download:
downloadBinder.pausedDownload();
break;
case R.id.btn_cancle_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(this, "You have denied the permission, app will finished", Toast.LENGTH_SHORT).show();
finish();
}
break;
default:
break;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(connection);
}
}
可以看到我们先创建了一个ServiceConnection的匿名类,然后在onServiceConnected()方法中获得了DownloadBinder实例,有了这个实例,我们就可以调用DownloadService中的各种方法了。
在onCreate()方法中,我们通过startService和bindService()方法启动和绑定服务。启动服务可以保证DownloadService一直在后台运行,绑定服务可以让DownloadPracticeActivity和DownloadService进行通信。除此之外,还要申请下写sd卡的权限WRITE_EXTERNAL_STORAGE。在点击事件中,如果点击下载按钮,就调用DownloadBinder中的startDownload()方法,并把url传入,其他的类似。最后需要注意的是要解绑服务,不然会造成内存泄露。
重新运行app,查看效果:
综合这个练习,总结一下,最近的学习中掌握了很多和Service相关的重要知识点,包括Android多线程编程,服务的基本用法,服务的生命周期,前台服务等。同时至此,我们已经将Android的四大组件都学习完了。