下載這個需求時常遇到,以前是版本更新下載單個apk包苦始,用okhttp+service或者系統(tǒng)提供的DownloadManager實現(xiàn)即可寞钥,方便快捷,不涉及多線程多任務下載陌选,特別是DownloadManager提供了完善的斷點理郑、狀態(tài)保存蹄溉、網(wǎng)絡判斷等功能,非常適合單一任務的下載情況您炉,但遇到批量下載(類似迅雷的下載)以上的方案就略顯不足了柒爵。如果全部自己來實現(xiàn)多任務、多線程赚爵、斷點續(xù)傳棉胀、暫停等功能,那工作量還是很大的冀膝,除非所開發(fā)的項目是專業(yè)下載的app唁奢,不然還是別造這個輪子了,就像我現(xiàn)在做的項目窝剖,批量下載只是app中一個小小的功能而已麻掸,所以我選擇用第三方庫。我采用的方案是Aria赐纱,也看過流利說的開源方案脊奋,但是那個庫很久沒維護,且使用復雜疙描,就選擇了aria诚隙。目前來看符合項目的需求,下載效果如下:
我采用service+notification對aria進行了封裝起胰,簡化外部調(diào)用最楷,直接看代碼(項目引入請看aria文檔):
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.Build;
import android.os.CountDownTimer;
import android.os.IBinder;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.arialyy.annotations.Download;
import com.arialyy.aria.core.Aria;
import com.arialyy.aria.core.download.DownloadEntity;
import com.arialyy.aria.core.download.DownloadReceiver;
import com.arialyy.aria.core.task.DownloadTask;
import com.orhanobut.logger.Logger;
import java.io.File;
import java.util.List;
/**
* 集成aria框架的下載service,主要功能:
* 1待错、提供前臺通知及任務進度通知展示
* 2、下載任務查重
* 3烈评、向外部提供進度更新接口
* <p>
* 添加下載任務直接調(diào)用靜態(tài)方法{@link #download(Context c, String u, String e)}
* 在任務列表需要顯示進度的頁面bindService火俄,通過#OnUpdateStatusListener更新數(shù)據(jù)
* <p>
* aria文檔:https://aria.laoyuyu.me/aria_doc/start/start.html
* Created by ly on 2021/7/19 14:09
*/
public class AriaService extends Service {
private static final String URL = "url";
private static final String EXTRA = "extra";
private static final int FOREGROUND_NOTIFY_ID = 1;
private static final int PROGRESS_NOTIFY_ID = 2;
private NotificationUtils notificationUtils;
private static volatile boolean isForegroundSuc;
private boolean timerFlag;
private OnUpdateStatusListener onUpdateStatusListener;
/**
* 添加下載任務
*
* @param url 下載鏈接
* @param extra 展示列表需要保存到數(shù)據(jù)庫的額外數(shù)據(jù)
*/
public static void download(@NonNull Context context, @NonNull String url, String extra) {
if (!TextUtils.isEmpty(url)) {
Intent intent = new Intent(context, AriaService.class);
intent.putExtra(URL, url);
intent.putExtra(EXTRA, extra);
if (!isForegroundSuc) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//android8.0以上通過startForegroundService啟動service
context.startForegroundService(intent);
} else {
context.startService(intent);
}
} else {
context.startService(intent);
}
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
Logger.d("onBind>>>");
return new MBinder();
}
public class MBinder extends Binder {
public AriaService getService() {
return AriaService.this;
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Logger.d("onStartCommand>>>");
//只處理start啟動并且傳入下載url的情況
if (intent != null && intent.hasExtra(URL)) {
String url = intent.getStringExtra(URL);
String extra = intent.getStringExtra(EXTRA);
if (!isNeedlessDownload(url)) {
File file = FUtils.createFileIsNotExist(new File(Constants.PATH_DOWNLOAD + System.currentTimeMillis()));
long taskId = Aria()
.load(url)
.setExtendField(extra)//自定義數(shù)據(jù)
.setFilePath(file.getAbsolutePath())//設置文件保存的完整路徑
.create();//啟動下載
if (taskId > 0) {
//如果任務創(chuàng)建成功,則開啟前臺service讲冠,開始下載
startForeground();
} else {
ToastUtil.showShort(R.string.task_create_fail);
}
}
/*
* 用startForegroundService啟動后5s內(nèi)還沒有startForeground表示沒有下載任務瓜客,則自動銷毀service(否則O及以上的系統(tǒng)會anr)
* 該操作對用戶不可見(startForeground后立馬stop了),代價就是創(chuàng)建了一個空service竿开,好處就是外部調(diào)用便利谱仪。
*
* 以下情況可以移除該操作:
* 1、不在service內(nèi)做下載查重工作
* 2否彩、不采用aria下載(aria需要傳入this,不靈活疯攒。但目前沒發(fā)現(xiàn)其他更好的方案,無奈列荔。敬尺、枚尼、)
*/
if (!timerFlag) {
timerFlag = true;
new CountDownTimer(4500, 4500) {
public void onTick(long millisUntilFinished) {
}
public void onFinish() {
if (!isForegroundSuc) {
startForeground();
stopSelf();
}
}
}.start();
}
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onCreate() {
super.onCreate();
Logger.d("onCreate>>>" + this);
Aria.get(this).getDownloadConfig()
.setUseBlock(true)
.setMaxTaskNum(2)
.setConvertSpeed(false)
.setUpdateInterval(500);
Aria().register();
notificationUtils = new NotificationUtils(this)
.setNotifyId(PROGRESS_NOTIFY_ID)
.setTitle(R.string.downloading)
.setPendingClassName("com.xxx.DownloadTaskActivity");
notificationUtils.getBuilder()
.setOngoing(true)//設置通知不可被取消
.setOnlyAlertOnce(true)
.setTicker(getString(R.string.downloading));
}
private void startForeground() {
if (!isForegroundSuc) {
startForeground(FOREGROUND_NOTIFY_ID, notificationUtils.build());
isForegroundSuc = true;
}
}
@Override
public void onDestroy() {
super.onDestroy();
Logger.w("onDestroy>>>");
isForegroundSuc = false;
timerFlag = false;
Aria().unRegister();
}
/**
* 判斷本地是否存在,避免重復添加到列表
*/
private boolean isNeedlessDownload(String url) {
boolean isExist = Aria().taskExists(url);
if (isExist) {
DownloadEntity entity = Aria().getFirstDownloadEntity(url);
isExist = new File(entity.getFilePath()).exists();
if (!isExist) {//文件不存在了砂吞,則移除記錄
Aria().load(entity.getId()).removeRecord();
} else {
Logger.w("該任務已存在:" + url);
}
}
return isExist;
}
private void update(DownloadTask task) {
if (onUpdateStatusListener != null)
onUpdateStatusListener.update(task);
}
private void notification(DownloadTask task, boolean isCompleted) {
if (isCompleted) {
//全部下載完成后署恍,重新newBuilder、用戶可選擇移除通知
notificationUtils.newBuilder().setTitle(R.string.all_download_completed).setContent("");
notificationUtils.getBuilder().setTicker(getString(R.string.all_download_completed));
} else {
//中間狀態(tài)的通知設置為靜默
notificationUtils.getBuilder().setNotificationSilent();
List<DownloadEntity> allTaskList = Aria().getTaskList();
List<DownloadEntity> completedList = Aria().getAllCompleteTask();
int taskNum = allTaskList == null ? 0 : allTaskList.size();
int completeNum = completedList == null ? 0 : completedList.size();
notificationUtils.setTitle(getString(R.string.download_progress) + completeNum + "/" + taskNum)
.setContent(getString(R.string.cur_download_task) + task.getTaskName());
}
notificationUtils.send();
}
public void setOnUpdateStatusListener(OnUpdateStatusListener onUpdateStatusListener) {
this.onUpdateStatusListener = onUpdateStatusListener;
}
public interface OnUpdateStatusListener {
void update(DownloadTask task);
}
public DownloadReceiver Aria() {
return Aria.download(this);
}
public void cancelNotification() {
notificationUtils.cancel(FOREGROUND_NOTIFY_ID);
notificationUtils.cancel();
}
//-----------aria框架回調(diào)-------------
@Download.onTaskPre
public void onTaskPre(DownloadTask task) {
update(task);
}
@Download.onTaskStart
public void onTaskStart(DownloadTask task) {
update(task);
notification(task, false);
}
@Download.onTaskStop
public void onTaskStop(DownloadTask task) {
update(task);
}
@Download.onTaskResume
public void onTaskResume(DownloadTask task) {
update(task);
notification(task, false);
}
@Download.onTaskCancel
public void onTaskCancel(DownloadTask task) {
update(task);
}
@Download.onTaskFail
public void onTaskFail(DownloadTask task) {
update(task);
}
@Download.onTaskComplete
public void onTaskComplete(DownloadTask task) {
Logger.i("onTaskComplete>>>>" + task.getTaskName());
update(task);
List<DownloadEntity> list = Aria().getAllNotCompleteTask();
List<DownloadEntity> completedList = Aria().getAllCompleteTask();
int unCompleteNum = list == null ? 0 : list.size();
if (unCompleteNum == 0 && completedList != null && !completedList.isEmpty()) {
notification(task, true);
ToastUtil.showShort(R.string.all_download_completed);
//移除前臺通知
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(true);
isForegroundSuc = false;
}
//全部完成蜻直,結束service
stopSelf();
} else {
notification(task, false);
}
}
@Download.onTaskRunning
public void onTaskRunning(DownloadTask task) {
update(task);
}
}
service中有一些基礎的工具類沒有貼出盯质,替換成你自己的即可。
外部只需調(diào)用內(nèi)部的download方法即可(最好自己先處理一下文件讀寫權限)概而,需要注意的是DownloadItem 是顯示用的額外實體類呼巷,傳入后aria會把它與下載任務關聯(lián)并以string的形式保存到數(shù)據(jù)庫:
DownloadItem downloadIte = new DownloadItem();
downloadIte.taskNameDesc = "test";
downloadIte.coverPic = "https://t7.baidu.com/it/u=2621658848,3952322712&fm=193&f=GIF.jpg";
AriaService.download(CloudMineActivity.this, url, new Gson().toJson(downloadIte));
通知工具類還是貼一下吧:
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.text.TextUtils;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import java.util.Random;
/**
* Created by ly on 2021/7/19 17:04
*/
public class NotificationUtils {
private static final int MAX = 100;
private int notifyId;
private String channelId;
private NotificationCompat.Builder builder;
private final NotificationManager notificationManager;
private NotificationManagerCompat notificationManagerCompat;
private String title, content;
private int progress;
private PendingIntent pendingIntent;
private final Context mContext;
public NotificationUtils(@NonNull Context context) {
this.mContext = context;
notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
newBuilder();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//創(chuàng)建通知渠道
CharSequence name = "default";
String description = "default";
int importance = NotificationManager.IMPORTANCE_HIGH;//重要性級別 這里用默認的
NotificationChannel mChannel = new NotificationChannel(getChannelId(), name, importance);
mChannel.setDescription(description);//渠道描述
mChannel.enableLights(true);//是否顯示通知指示燈
mChannel.enableVibration(true);//是否振動
notificationManager.createNotificationChannel(mChannel);//創(chuàng)建通知渠道
} else {
notificationManagerCompat = NotificationManagerCompat.from(mContext);
}
}
public NotificationUtils newBuilder() {
builder = new NotificationCompat.Builder(mContext, getChannelId());
return this;
}
public NotificationUtils send() {
return send(notifyId);
}
public NotificationUtils send(int notifyId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationManager.notify(notifyId, build());
} else {
notificationManagerCompat.notify(notifyId, build());
}
return this;
}
public Notification build() {
builder.setSmallIcon(R.mipmap.ic_launcher)
.setPriority(NotificationCompat.PRIORITY_MAX)
//鈴聲、閃光到腥、震動均系統(tǒng)默認
.setDefaults(Notification.DEFAULT_ALL)
.setAutoCancel(true)
.setContentTitle(title)
.setContentText(content);
if (progress > 0 && progress < MAX) {
builder.setProgress(MAX, progress, false);
} else {
builder.setProgress(0, 0, false);
}
if (pendingIntent != null) {
builder.setContentIntent(pendingIntent).setAutoCancel(true);
builder.setFullScreenIntent(pendingIntent, true);
}
return builder.build();
}
public void cancel() {
cancel(notifyId);
}
public void cancel(int notifyId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationManager.cancel(notifyId);
} else {
notificationManagerCompat.cancel(notifyId);
}
}
public NotificationUtils setTitle(@StringRes int title) {
this.title = mContext.getString(title);
return this;
}
public NotificationUtils setContent(@StringRes int content) {
this.content = mContext.getString(content);
return this;
}
public NotificationUtils setTitle(String title) {
this.title = title;
return this;
}
public NotificationUtils setContent(String content) {
this.content = content;
return this;
}
public NotificationUtils setProgress(@IntRange(from = 0, to = MAX) int progress) {
this.progress = progress;
return this;
}
public NotificationUtils setPendingClass(Class<?> cls) {
Intent intent = new Intent(mContext, cls);
pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
return this;
}
public NotificationUtils setPendingClassName(String cls) {
Intent intent = new Intent();
intent.setClassName(mContext, cls);
pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
return this;
}
public NotificationUtils setNotifyId(int notifyId) {
this.notifyId = notifyId;
return this;
}
public NotificationUtils setChannelId(String channelId) {
this.channelId = channelId;
return this;
}
public int getNotifyId() {
if (notifyId == 0)
this.notifyId = new Random().nextInt() + 1;
return notifyId;
}
public String getChannelId() {
if (TextUtils.isEmpty(channelId))
this.channelId = String.valueOf(new Random().nextInt() + 1);
return channelId;
}
public String getTitle() {
return title;
}
public String getContent() {
return content;
}
public int getProgress() {
return progress;
}
public NotificationCompat.Builder getBuilder() {
return builder;
}
}
到此朵逝,下載的全部代碼都分享完畢,說一下我對aria的一些看法:
1乡范、框架335kb配名,挺大的了,里面包含了http晋辆、下載上傳等功能渠脉,不精簡。作者如果能把http瓶佳、上傳等功能分離抽出來做成可選依賴會更好芋膘。
2、目前沒找到批量添加下載任務的api(發(fā)現(xiàn)的朋友請留言告訴我
)