前言
在学习完《第一行代码》的下载最佳实践后,打算利用Android的基础知识将此完善成一个多任务,断点,离线保存,支持后台的下载示例。适合刚把Android基础知识学完不知道怎么组合运用的初学者。用到的
Android知识有:
1.OkHttp断点下载;
2.Activity与Service通信;
3.权限获取;
4.RecyclerView和Adapter实现列表;
5.SQLite保存数据;
6.AsyncTask异步下载。
效果演示
下载、暂停、后台下载功能点
1.能多条任务同时下载;
2.支持暂停,继续下载;
3.finish掉Activity后能在后台下载;
4.stopService退出程序时,使用数据库保存进度。
结构
1.结构图
结构图.PNG2.结构分析
(1)UI部分
MainActivity中通过调用notifyDataSetChanged()等方法刷新RecyclerView的数据;当用户点击了RecyclerView中的StartButton和CancelButton,通过OnItemButtonClickListener回调MainActivity。
(2)Activity与Service通信
由于下载是个耗时操作,我们要使用Service来做数据和逻辑操作。使用Binder和OnTaskDataChangeListener实现Activity与Service通信。
(3)Service调用SQLite
在Service中调用DatabaseManager将当前的下载数据保存。
(4)DownloadManager
使用DownloadManager管理下载任务。Service调用DownloadManager做下载操作,DownloadManager通过DownloadView回调Service做数据更新。
(5)DownloadAsyncTask异步下载
DownloadManager用HashMap管理DownloadAsyncTask做多任务下载,DownloadAsyncTask用DownLoadListener将每一个任务的下载状态回调给DownloadManager。
具体实现
1.获取权限
实现下载功能我们需要“网络权限” 和 “存储读写权限”。
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission>
Android 6.0 以上还要在Activity请求权限。较为基础没什么多说的,直接上代码:
MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//获取权限
int readStorageCheck = ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE);
if (readStorageCheck == PackageManager.PERMISSION_GRANTED) {
initUI();
} else {
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 0);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
//当权限获得时
if (grantResults.length > 0 &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
initUI();
} else {
if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
//弹出对话框提示用户接收权限
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setMessage("程序要获得权限后才能运行");
dialog.setCancelable(false);
dialog.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//请求读取手机存储的权限
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 0);
}
});
dialog.create().show();
}
}
}
2.Activity与Service通信
(1)创建DownloadBinder
Activity需要Service做的事情有:
1.添加新任务;
2.点击了“开始”按钮;
3.点击了“暂停”按钮;
4.点击了“取消”按钮;
5.点击stopService退出程序时,使用数据库保存进度。
因此在DownloadBinder定义5个方法,使用url做参数,可以确定当前是哪一个下载任务被用户操作。
DownloadService.java
class DownloadBinder extends Binder {
//点击了开始按钮
public void startDownload(String url) {
}
//点击了暂停按钮
public void pauseDownload(String url) {
}
//点击了取消按钮
public void cancelDownload(String url) {
}
//新建任务
public void newTask(String url) {
}
//stopService退出程序
public void saveProgress() {
}
}
(2)创建OnTaskDataChangeListener
使用OnTaskDataChangeListener让Service回调Activity
DownloadService.java
interface OnTaskDataChangeListener {
//应用打开时,Activity初始化数据
void onInitFinish(List<Task> list);
//添加了新任务
void onDataInsert(int position);
//任务的状态发送了变化,例如:更新下载进度,开始和暂停转换
void onDataChange(int position);
//任务被取消
void onDataRemove(int position);
}
(3)绑定Service
当Activity启动时,绑定Service。同时调用mServiceBinder.setListener(MainActivity.this);将MainActivity实现的OnTaskDataChangeListener接口传到Service。这时Service就知道Activity启动了。这时候在setListener()方法中调用mListener.onInitFinish(mTasks)给Actvity初始化数据界面。
MainActivity.java
//绑定Service,实现Activity和Service通信
private DownloadService.DownloadBinder mServiceBinder;
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mServiceBinder = (DownloadService.DownloadBinder) service;
mServiceBinder.setListener(MainActivity.this);
}
@Override
public void onServiceDisconnected(ComponentName name) {
mServiceBinder = null;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//绑定Service
Intent serviceIntent = new Intent(this, DownloadService.class);
startService(serviceIntent);
bindService(serviceIntent, mConnection, BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
unbindService(mConnection);
super.onDestroy();
}
DownloadService.java
private ArrayList<Task> mTasks;
private OnTaskDataChangeListener mListener;
private DatabaseManager mDataBaseManager;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return new DownloadBinder();
}
@Override
public boolean onUnbind(Intent intent) {
mListener = null;
return super.onUnbind(intent);
}
class DownloadBinder extends Binder {
public void setListener(OnTaskDataChangeListener listener) {
mListener = listener;
if (mTasks == null) {
mTasks = new ArrayList<>();
//在数据库中取得数据
mTasks.addAll(mDataBaseManager.query());
}
mListener.onInitFinish(mTasks);
}
}
3.Service中的逻辑操作
(1)创建数据java bean
Task.java
public class Task implements Serializable {
private String url;
private String name;
private int progress;
private boolean isDownloading;
public Task(String url, String name) {
this.url = url;
this.name = name;
isDownloading = true;
}
public Task(String url, String name, int progress) {
this.url = url;
this.name = name;
this.progress = progress;
this.isDownloading = false;
}
}
(2)定义Service中的成员变量
由于我们要实现Activity被销毁后还能继续在后台下载,因此将数据列表ArrayList<Task> mTasks的变化放在Service中,而不是在Activity中。使用DatabaseManager mDataBaseManager;做数据库操作。使用DownloadManager mDownloadManager;做下载操作。
DownloadService.java
private ArrayList<Task> mTasks;
private OnTaskDataChangeListener mListener;
private DatabaseManager mDataBaseManager;
private DownloadManager mDownloadManager;
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate: ");
if (mDownloadManager == null) {
Log.d(TAG, "onCreate: init mDownloadManager");
mDownloadManager = new DownloadManager(this);
}
if (mDataBaseManager == null) {
Log.d(TAG, "onCreate: init mDataBaseManager");
mDataBaseManager = DatabaseManager.getInstance(getApplicationContext());
}
//后台下载
Notification.Builder builder = new Notification.Builder(getApplicationContext());
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setContentText("下载");
builder.setContentTitle("下载");
Notification notification = builder.build();
// notification.flags = Notification.FLAG_FOREGROUND_SERVICE;
startForeground(0, notification);
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.notify(0, notification);
}
@Override
public void onDestroy() {
Log.d(TAG, "onDestroy: ");
stopForeground(true);
super.onDestroy();
}
(3)实现DownloadBinder的方法
DownloadService.java
class DownloadBinder extends Binder {
public void startDownload(String url) {
mDownloadManager.addDownloadTask(url);
}
public void pauseDownload(String url) {
mDownloadManager.pauseDownload(url);
}
public void cancelDownload(String url) {
mDownloadManager.cancelDownload(url);
}
public void newTask(String url) {
//判断任务是否已经存在
for (Task t : mTasks) {
if (t.getUrl().equals(url)) {
Toast.makeText(getApplicationContext(), "任务已经存在", Toast.LENGTH_SHORT).show();
return;
}
}
String name = url.substring(url.lastIndexOf("/") + 1);
Task task = new Task(url, name);
mTasks.add(task);
if (mListener != null) {
mListener.onDataInsert(mTasks.size() - 1);
}
startDownload(url);
}
public void saveProgress() {
for (Task task : mTasks) {
Log.d(TAG, "saveProgress: "+task.toString());
//停止下载
if (task.getProgress() < 100) {
if(task.isDownloading()){
mDownloadManager.pauseDownload(task.getUrl());
}
}
//保存到数据库
if (mDataBaseManager.query(task.getUrl()) == null) {
mDataBaseManager.insert(task);
}else{
mDataBaseManager.update(task.getUrl(), task.getProgress());
}
}
}
}
(4)用DownloadView作DownloadManager下载状态的回调
DownloadView.java
public interface DownloadView {
void onDownloadPause(String url);
void updateProgress(String url, int progress);
void onFail(String url);
void onCancel(String url);
}
让Service实现DownloadView的方法,当DownloadManager的下载状态变化时,回调Service更新task数据。
DownloadService.java
private ArrayList<Task> mTasks;
private OnTaskDataChangeListener mListener;
private DatabaseManager mDataBaseManager;
private DownloadManager mDownloadManager;
@Override
public void onDownloadPause(String url) {
int position = getPosition(url);
Task task = mTasks.get(position);
task.setDownloading(false);
if (mListener != null) {
mListener.onDataChange(position);
}
}
@Override
public void updateProgress(String url, int progress) {
int position = getPosition(url);
Task task = mTasks.get(position);
task.setProgress(progress);
if (progress < 100) {
task.setDownloading(true);
} else {
task.setDownloading(false);
}
if (mListener != null) {
mListener.onDataChange(position);
}
Log.d(TAG, "updateProgress: " + progress);
}
@Override
public void onFail(String url) {
int position = getPosition(url);
Task task = mTasks.get(position);
task.setProgress(-1);
task.setDownloading(false);
if (mListener != null) {
mListener.onDataChange(position);
}
}
@Override
public void onCancel(String url) {
int position = getPosition(url);
Task task = mTasks.get(position);
mTasks.remove(position);
if (mDataBaseManager.query(task.getUrl()) != null) {
mDataBaseManager.delete(url);
}
if (mListener != null) {
mListener.onDataRemove(position);
}
}
/**
* 通过url找到当前task在list的position
*
* @param url
* @return
*/
private int getPosition(String url) {
for (int i = 0; i < mTasks.size(); i++) {
Task task = mTasks.get(i);
if (url.equals(task.getUrl())) {
return i;
}
}
return -1;
}
4.DownloadManager管理下载任务
在DownloadManager使用HashMap管理DownloadAsyncTask。
1.在事件“新建任务”,“开始下载”中,我们都要新建DownloadAsyncTask;
2.当正在下载时,对于事件“暂停”和“取消”,我们只要根据url在map中找到当前asyncTask,将其暂停和取消。由于asyncTask的生命周期已经完成了,我们要将其在map中remove。
3.当用户在暂停状态下点击了取消按钮,由于当前任务已经在map中remove,要重新新建DownloadAsyncTask,才能将其取消。
根据以上分析,可以抽象出以下方法,并让DownloadManager继承并实现。
DownloadModel.java
public abstract class DownloadModel {
abstract void addDownloadTask(String url);
abstract void pauseDownload(String url);
abstract void cancelDownload(String url);
}
DownloadManager.java
public class DownloadManager extends DownloadModel {
private HashMap<String, DownloadAsyncTask> mMap = new HashMap<>();
private DownloadView mView;
public DownloadManager(DownloadView view) {
mView = view;
}
@Override
public void addDownloadTask(final String url) {
DownloadAsyncTask asyncTask = new DownloadAsyncTask(new DownloadAsyncTask.DownLoadListener() {
@Override
public void onDownloadPause() {
mMap.remove(url);
mView.onDownloadPause(url);
}
@Override
public void updateProgress(int progress) {
mView.updateProgress(url, progress);
}
@Override
public void onFail() {
mMap.remove(url);
mView.onFail(url);
}
@Override
public void onCancel() {
mMap.remove(url);
mView.onCancel(url);
}
@Override
public void onFinish() {
mMap.remove(url);
}
});
mMap.put(url, asyncTask);
//实现多任务下载,开始任务
asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url);
}
@Override
public void pauseDownload(String url) {
DownloadAsyncTask task = mMap.get(url);
task.setPause();
mMap.remove(task);
}
@Override
public void cancelDownload(String url) {
DownloadAsyncTask task = mMap.get(url);
//当未下载时点击取消,要新建AsyncTask
if (task == null) {
addDownloadTask(url);
}
task.setCancel();
mMap.remove(task);
}
}
同时我们还需要在DownloadAsyncTask创建DownloadListener接口回调将当前AsyncTask的下载状态状态返回给DownloadManager,并由DownloadManager再通过url返回给Service。
DownloadAsyncTask.java
public interface DownLoadListener {
void onDownloadPause();
void updateProgress(int progress);
void onFail();
void onCancel();
void onFinish();
}
5.DownloadAsyncTask实现下载
(1)创建类 DownloadAsyncTask
DownloadAsyncTask extends AsyncTask<String, Integer, Integer>,其中String为下载的url,第一个Integer为下载进度,第二个Integer为下载状态。
(2)下载的状态标识。
DownloadAsyncTask.java
private static final int STATUS_SUCCEED = 1;
private static final int STATUS_PAUSED = 2;
private static final int STATUS_CANCELED = 3;
private static final int STATUS_FAILED = 4;
当AsyncTask正在doInBackground()时,用户点击暂停或取消时,使用isPause或isCancel中断任务
DownloadAsyncTask.java
private boolean isPause = false;
private boolean isCancel = false;
public void setPause() {
isPause = true;
}
public void setCancel() {
isCancel = true;
}
(3)重写doInBackground方法
下载要用到OkHttp,引入闭包
compile 'com.squareup.okhttp3:okhttp:3.4.1'
首先得到File文件。当新建AsyncTask时,有可能是“下载”,也有可能是在暂停状态下“取消”。若是取消,将file文件删除,并结束任务,返回状态STATUS_CANCELED。若是下载,通过file.exists()判断是重头下载还是继续下载,并得到已下载进度downloadedLength。然后通过OkHttp获取内容的大小contentLength。若contentLength为0,则无法下载;若contentLength == downloadedLength则表示下载完成。
DownloadAsyncTask.java
@Override
protected Integer doInBackground(String... params) {
String url = params[0];
String name = url.substring(url.lastIndexOf("/"));
long downloadedLength = 0;
String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
File file = new File(directory + name);
//判断任务是开始还是取消
if(isCancel){
if(file.exists()){
file.delete();
}
return STATUS_CANCELED;
}
if(file.exists()){
downloadedLength = file.length();
}
long contentLength = getContentLength(url);
//无法下载
if (contentLength == 0) {
return STATUS_FAILED;
} else if (contentLength == downloadedLength) {
return STATUS_SUCCEED;
}
//开始下载
isPause = false;
isCancel = false;
InputStream is = null;
RandomAccessFile saveFile = null;
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().
addHeader("RANGE", "bytes=" + downloadedLength + "-") //指定从哪一个字节下载
.url(url).build();
try {
Response response = client.newCall(request).execute();
//写入到本地
if (response != null) {
Log.d(TAG, "doInBackground: response not null");
is = response.body().byteStream();
saveFile = new RandomAccessFile(file, "rw");
saveFile.seek(downloadedLength);
int len;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != -1) {
if (isPause) {
return STATUS_PAUSED;
} else if (isCancel) {
if(file.exists()){
file.delete();
}
return STATUS_CANCELED;
}
//获取已下载的进度
saveFile.write(buffer, 0, len);
downloadedLength += len;
int progress = (int) (downloadedLength * 100 / contentLength);
publishProgress(progress);
}
response.body().close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
if (saveFile != null) {
saveFile.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
if (contentLength == downloadedLength) {
return STATUS_SUCCEED;
}
return STATUS_FAILED;
}
private long getContentLength(String url) {
long contentLength = 0;
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(url).build();
Response response = null;
try {
response = client.newCall(request).execute();
} catch (IOException e) {
e.printStackTrace();
}
if (response != null && response.isSuccessful()) {
contentLength = response.body().contentLength();
response.close();
} else {
Log.d(TAG, "getContentLength: response null");
}
return contentLength;
}
源代码
https://github.com/Luckychuan/MultiThreadDownloadDemo
·
网友评论