随着Google规范政策的不断完善,后台服务这个东东已经不再被允许了,那么,该如何去实现一个后台下载的功能呢?
注意:这里的后台是指应用进入后台,并非杀掉进程也能下载哈!(DownloadManager除外)
这次的示例将使用3种方式进行:前台服务、WorkManager和DownloadManager
一、前台服务
Android14开始对前台服务有更加严格的要求,具体细节请参考:前台服务类型
实现方式,可通过继承IntentService,Service等启动前台服务
示例:
public class MyIntentService extends IntentService {
private static final String TAG = "MyIntentService";
private static final int DOWNLOAD_NOTIFY_ID = 1000;
/**
* 服务启动
*/
public static void startService(Context context) {
Log.i("测试", "startService:called!");
NotifyUtil.createNotification(context);
Intent intent = new Intent(context, MyIntentService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
} else {
context.startService(intent);
}
}
/**
* 服务停止
*/
public static void stopService(Context context) {
Intent intent = new Intent(context, MyIntentService.class);
context.stopService(intent);
}
public MyIntentService() {
super("MyIntentService");
}
@Override
public void onCreate() {
super.onCreate();
// 获取服务通知
Notification notification = NotifyUtil.getNotification();
//将服务置于启动状态 ,NOTIFICATION_ID指的是创建的通知的ID
startForeground(DOWNLOAD_NOTIFY_ID, notification);
}
@Override
protected void onHandleIntent(Intent intent) {
Log.i("测试", "onHandleIntent:called!");
//如果想要实现同步队列,可以使用ExecutorService executorService = Executors.newSingleThreadExecutor();
}
}
耗时较长和复杂的逻辑可以在自带线程的onHandleIntent中处理
AndroidManifest.xml清单文件中加入前台服务的配置
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<service android:name=".manager.MyIntentService" android:exported="false" />
其它网络请求权限
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
注意:
1.前台服务需要绑定一个Notification,而Notification有三个值是一定要设置的,不然就会报错
/**
* 注意:
* 在通知栏显示下载进度,以下三个属性值需要设置
* 否则会直接抛出异常
*/
builder.setContentTitle("Downloading...");
builder.setContentText("0%");
builder.setSmallIcon(id);
2.在Android13开始,通知权限需要动态申请了,否则接收不到通知,也就是说在通知栏显示进度的也无法显示,所以,记得申请通知权限
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
//动态申请的代码请自行百度
二、WorkManager
WorkManager是Google推荐使用的,它也可以实现后台下载,大概的原理是:类似于定时轮询检测的一个工作任务,每隔一段时间就会轮询检测一次工作任务是否还在进行,因此startWork是会存在多次回调的问题!
Google对WorkManager的介绍:WorkManager 调度任务
示例:
public class DownloadWorker extends ListenableWorker {
private static final String TAG = DownloadWorker.class.getSimpleName();
public DownloadWorker(@NonNull Context context, @NonNull WorkerParameters params) {
super(context, params);
}
@SuppressLint("RestrictedApi")
@NonNull
@Override
public ListenableFuture<Result> startWork() {
Log.i("测试", "startWork is called!");
WorkCallback callback = ConfigUtil.getInstance().getWorkCallback();
if (callback == null) {
SettableFuture<Result> future = SettableFuture.create();
future.set(Result.failure());
return future;
}
return callback.doWork();
}
@Override
public void onStopped() {
super.onStopped();
Log.i("测试", "onStopped is called!");
WorkCallback callback = ConfigUtil.getInstance().getWorkCallback();
if (callback != null) {
callback.onStopped();
}
}
}
注意:
建议直接继承ListenableWorker,上面说到startWork是会存在多次回调的问题,继承ListenableWorker 可以通过SettableFuture去设置成功或失败,失败后不会重复轮询去检查调用;但是如果继承的是Worker,当回调失败时,Worker会定时去轮询这个失败的任务,直到成功为止,这里如果逻辑处理不当,就有可能出现多个任务同时下载同一个资源的问题!
其它方法调用
//启动工作任务
private static void enqueue(Context context) {
Log.i("测试", "enqueue is called!");
// 创建网络状态变化约束条件,断网时自动停止,重新链接网络时,自动继续下载
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)//网络连接
.setRequiresBatteryNotLow(false)//设备电量低于指定阈值时仍会执
.setRequiresCharging(false)//设备不在充电时也会执行
.build();
OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(DownloadWorker.class)
.setConstraints(constraints)
.build();
WorkManager.getInstance(context).enqueue(request);
}
/**
* 停止所有 Worker
*
* @param context 上下文
*/
public static void stopWorker(Context context) {
Log.i(TAG, "stopWorker is called!");
if (context == null) {
Log.w(TAG, "stopWorker context == null!");
return;
}
WorkManager.getInstance(context).cancelAllWork();
}
/**
* 停止某个Worker
*
* @param context 上下文
* @param workId 获取 Worker 的 ID
*/
public static void stopWorker(Context context, String workId) {
Log.i(TAG, "stopWorker is called!");
if (context == null || TextUtils.isEmpty(workId)) {
Log.w(TAG, "stopWorker context == null or workId isEmpty!");
return;
}
WorkManager.getInstance(context).cancelWorkById(UUID.fromString(workId));
}
三、DownloadManager
系统自带的下载器,这玩意有点秀,是真的后台下载,杀掉进程也还会继续下载,直到资源下载完成,或调用了停止方法remove()
注意:
1.调用remove()方法不会立刻停止下载,会把当前资源下载完成才会停止,在某些版本上可能需要全部下载完成才能停止,蛋疼!!
2.不能暂停,接口已不能调用了!
示例:
public class MyDownloadManager {
private static DownloadManager downloadManager;
private static long downloadId;
public static void download(Context context) {
String url = "https://********";
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
request.setTitle("Download"); // 设置通知标题
request.setDescription("Downloading resource"); // 设置通知描述
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); // 设置下载完成后显示通知
// 设置下载文件的保存位置
File file = new File(context.getExternalFilesDir(null), "aa.apk");
request.setDestinationUri(Uri.parse("file://" + file.getPath()));
downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
downloadId = downloadManager.enqueue(request);
}
public static void stop() {
downloadManager.remove(downloadId);
}
示例图:
通知栏:
那么示例已经完成了,可以结合需求,自行添加逻辑,数据库等进行封装使用,根据需求各取所需哈!!感谢~~
完整demo地址:Android 后台下载功能demo