前言
前段时间写了一个篇APP自动更新下载的文章自动更新,一个方法搞定,使用系统的DownloadManager 方法超简洁的实现了apk的下载,不过有好多网友反映有一些机型上面这个方法无法实现下载,经过小编的实验在部分机型上确实会有这个问题,所以其中下载的部分只能通过其它方法搞定了。正好看到网上好多关于使用Retrofit实现下载并且监听进度的文章,并且我一直在看Retrofit的东西但是一直没有机会用到,所以我正好拿这个练练手,最终我使用Retrofit + OkHttp + RxBus + Notification + Service实现了这个自动更新下载apk的功能。Demo已经上传的github了大家可以下载下来自己看看,你不仅可以解决App更新的问题,并且可以通过实践了解到一些比较不错的技术。github地址:https://github.com/shanyao0/DownLoadManager,大家多多star和fork,谢谢。。。
原理
基本原理和使用方法跟自动更新,一个方法搞定一样大家不懂得可以参考这里,同样还是一个方法搞定超级简单,不过只是这次的逼格更高,机型兼容性更好。这次手动实现了下载和系统通知进度功能。
- Retrofit2和okhttp实现了apk的下载
- 自定义类实现Retrofit2的Callback类在里面通过IO流写入文件并且使用RxBus订阅下载进度
- 自定义类实现okhttp3的ResponseBody类并且在里面使用RxBus发布下载进度信息
- 在Service中使用Retrofit在后台下载文件
- 发送Notifaction到通知栏前台界面展示进度情况
所以我希望大家可以跟着我下面的实现步骤一步一步的实现这个功能,这样你不仅可以在你的项目中使用高逼格的技术,还可以对这些技术有一个比较初步的认识。
实现步骤
1. 创建UpdateManger管理类
这个类主要写了两个管理更新和弹框的方法,比较简单,跟自动更新,一个方法搞定的差不多除了下载部分
/** - 检测软件更新 */ public void checkUpdate(final boolean isToast) { /** * 在这里请求后台接口,获取更新的内容和最新的版本号 */ // 版本的更新信息 String version_info = "更新内容/n" + " 1. 车位分享异常处理/n" + " 2. 发布车位折扣格式统一/n" + " "; int mVersion_code = DeviceUtils.getVersionCode(mContext);// 当前的版本号 int nVersion_code = 2; if (mVersion_code < nVersion_code) { // 显示提示对话 showNoticeDialog(version_info); } else { if (isToast) { Toast.makeText(mContext, "已经是最新版本", Toast.LENGTH_SHORT).show(); } } } /** * 显示更新对话框 * * @param version_info */ private void showNoticeDialog(String version_info) { // 构造对话框 AlertDialog.Builder builder = new AlertDialog.Builder(mContext); builder.setTitle("更新提示"); builder.setMessage(version_info); // 更新 builder.setPositiveButton("立即更新", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); // 启动后台服务下载apk mContext.startService(new Intent(mContext, DownLoadService.class)); } }); // 稍后更新 builder.setNegativeButton("以后更新", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); Dialog noticeDialog = builder.create(); noticeDialog.show(); }
2. 初始化RxBus进行简单封装
RxBus的使用可以参考这里用RxJava实现事件总线
import rx.Observable; import rx.subjects.PublishSubject; import rx.subjects.SerializedSubject; import rx.subjects.Subject; public class RxBus { private static volatile RxBus mInstance; private final Subject<Object, Object> bus; private RxBus() { bus = new SerializedSubject<>(PublishSubject.create()); } /** * 单例RxBus * * @return */ public static RxBus getDefault() { RxBus rxBus = mInstance; if (mInstance == null) { synchronized (RxBus.class) { rxBus = mInstance; if (mInstance == null) { rxBus = new RxBus(); mInstance = rxBus; } } } return rxBus; } /** * 发送一个新事件 * * @param o */ public void post(Object o) { bus.onNext(o); } /** * 返回特定类型的被观察者 * * @param eventType * @param <T> * @return */ public <T> Observable<T> toObservable(Class<T> eventType) { return bus.ofType(eventType); } }
3. 自己定制ResponseBody类FileResponseBody
public class FileResponseBody extends ResponseBody { Response originalResponse; public FileResponseBody(Response originalResponse) { this.originalResponse = originalResponse; } @Override public MediaType contentType() { return originalResponse.body().contentType(); } @Override public long contentLength() {// 返回文件的总长度,也就是进度条的max return originalResponse.body().contentLength(); } @Override public BufferedSource source() { return Okio.buffer(new ForwardingSource(originalResponse.body().source()) { long bytesReaded = 0; @Override public long read(Buffer sink, long byteCount) throws IOException { long bytesRead = super.read(sink, byteCount); bytesReaded += bytesRead == -1 ? 0 : bytesRead; // 通过RxBus发布进度信息 RxBus.getDefault().post(new FileLoadingBean(contentLength(), bytesReaded)); return bytesRead; } }); } }
这个别问我问啥这样写,是从ophttp3的官方demo里面搞过来的东西,如果大家有精力可以研究下,到时别忘了告诉我一声,谢谢
4. 自己定制Callback类FileCallback
一个抽象类实现了Callback的onResponse方法并且在里面利用IO流把文件保存到了本地
定义了两个抽象方法让子类实现,onSuccess()当读写完成之后将文件回调给实现类以便安装apk,onLoading()在文件读写的过程中通过订阅下载的进度把进度信息progress和total回调给实现类以便在通知中实时显示进度信息
public abstract class FileCallback implements Callback<ResponseBody>{ /** * 订阅下载进度 */ private CompositeSubscription rxSubscriptions = new CompositeSubscription(); /** * 目标文件存储的文件夹路径 */ private String destFileDir; /** * 目标文件存储的文件名 */ private String destFileName; public FileCallback(String destFileDir, String destFileName) { this.destFileDir = destFileDir; this.destFileName = destFileName; subscribeLoadProgress();// 订阅下载进度 } /** * 成功后回调 */ public abstract void onSuccess(File file); /** * 下载过程回调 */ public abstract void onLoading(long progress, long total); /** * 请求成功后保存文件 */ @Override public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) { try { saveFile(response); } catch (Exception e) { e.printStackTrace(); } } /** * 通过IO流写入文件 */ public File saveFile(Response<ResponseBody> response) throws Exception { InputStream in = null; FileOutputStream out = null; byte[] buf = new byte[2048]; int len; try { File dir = new File(destFileDir); if (!dir.exists()) {// 如果文件不存在新建一个 dir.mkdirs(); } in = response.body().byteStream(); File file = new File(dir,destFileName); out = new FileOutputStream(file); while ((len = in.read(buf)) != -1){ out.write(buf,0,len); } // 回调成功的接口 onSuccess(file); unSubscribe();// 取消订阅 return file; }finally { in.close(); out.close(); } } /** * 订阅文件下载进度 */ private void subscribeLoadProgress() { rxSubscriptions.add(RxBus.getDefault() .toObservable(FileLoadingBean.class)// FileLoadingBean保存了progress和total的实体类 .onBackpressureBuffer() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1<FileLoadingBean>() { @Override public void call(FileLoadingBean fileLoadEvent) { onLoading(fileLoadEvent.getProgress(), fileLoadEvent.getTotal()); } })); } /** * 取消订阅,防止内存泄漏 */ private void unSubscribe() { if (!rxSubscriptions.isUnsubscribed()) { rxSubscriptions.unsubscribe(); } } }
保存了progress和total的实体类
public class FileLoadingBean { /** * 文件大小 */ long total; /** * 已下载大小 */ long progress; public long getProgress() { return progress; } public long getTotal() { return total; } public FileLoadingBean(long total, long progress) { this.total = total; this.progress = progress; } }
5. 在后台Service中利用Retrofit2和okhttp下载并安装apk,同时发送通知在前台展示下载进度
Retrofit和okhttp的使用请参考鸿洋大神的Retrofit2 完全解析 探索与okhttp之间的关系
通知Notification大家直接参考官网Notification就行
public class DownLoadService extends Service { /** * 目标文件存储的文件夹路径 */ private String destFileDir = Environment.getExternalStorageDirectory().getAbsolutePath() + File .separator + "M_DEFAULT_DIR"; /** * 目标文件存储的文件名 */ private String destFileName = "shan_yao.apk"; private Context mContext; private int preProgress = 0; private int NOTIFY_ID = 1000; private NotificationCompat.Builder builder; private NotificationManager notificationManager; private Retrofit.Builder retrofit; /** * 为什么在这个方法调用下载的逻辑?而不是onCreate?我在下面有解释 */ @Override public int onStartCommand(Intent intent, int flags, int startId) { mContext = this; loadFile(); return super.onStartCommand(intent, flags, startId); } @Nullable @Override public IBinder onBind(Intent intent) { return null; } /** * 下载文件 */ private void loadFile() { initNotification(); if (retrofit == null) { retrofit = new Retrofit.Builder(); } // 使用Retrofit进行文件的下载 retrofit.baseUrl("http://112.124.9.133:8080/parking-app-admin-1.0/android/manager/adminVersion/") .client(initOkHttpClient()) .build() .create(IFileLoad.class) .loadFile() .enqueue(new FileCallback(destFileDir, destFileName) { @Override public void onSuccess(File file) { Log.e("zs", "请求成功"); // 安装软件 cancelNotification(); installApk(file); } @Override public void onLoading(long progress, long total) { Log.e("zs", progress + "----" + total); updateNotification(progress * 100 / total);// 更新前台通知 } @Override public void onFailure(Call<ResponseBody> call, Throwable t) { Log.e("zs", "请求失败"); cancelNotification();// 取消通知 } }); } public interface IFileLoad { @GET("download") Call<ResponseBody> loadFile(); } /** * 安装软件 * * @param file */ private void installApk(File file) { Uri uri = Uri.fromFile(file); Intent install = new Intent(Intent.ACTION_VIEW); install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); install.setDataAndType(uri, "application/vnd.android.package-archive"); // 执行意图进行安装 mContext.startActivity(install); } /** * 初始化OkHttpClient * * @return */ private OkHttpClient initOkHttpClient() { OkHttpClient.Builder builder = new OkHttpClient.Builder(); builder.connectTimeout(100000, TimeUnit.SECONDS); builder.networkInterceptors().add(new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { Response originalResponse = chain.proceed(chain.request()); return originalResponse .newBuilder() .body(new FileResponseBody(originalResponse))//将自定义的ResposeBody设置给它 .build(); } }); return builder.build(); } /** * 初始化Notification通知 */ public void initNotification() { builder = new NotificationCompat.Builder(mContext) .setSmallIcon(R.mipmap.ic_launcher)// 设置通知的图标 .setContentText("0%")// 进度Text .setContentTitle("QQ更新")// 标题 .setProgress(100, 0, false);// 设置进度条 notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);// 获取系统通知管理器 notificationManager.notify(NOTIFY_ID, builder.build());// 发送通知 } /** * 更新通知 */ public void updateNotification(long progress) { int currProgress = (int) progress; if (preProgress < currProgress) { builder.setContentText(progress + "%"); builder.setProgress(100, (int) progress, false); notificationManager.notify(NOTIFY_ID, builder.build()); } preProgress = (int) progress; } /** * 取消通知 */ public void cancelNotification() { notificationManager.cancel(NOTIFY_ID); } }
需要注意的几个地方
-
为什么在onStartCommand里面下载apk而不是onCreate里面
这个跟service的生命周期有关系,onStartCommand()在每次调用startService时候都会调用一次,而onCreate()方法只有在服务第一次启动的时候才会掉用。当一个服务已经开启了那么再次调用startService不会在调用onCreate()方法,而onStartCommand()会被再次调用。
-
发送通知设置进度用的是setProgress,这个方法只有在4.0以上才能用
如果不用这个方法想要兼容低版本,就只能使用自定义的Notification,然后自己创建一个含有ProgressBar的layout布局,但是自定义的Notification在不同的系统中的适配处理太麻烦了,因为不同的系统的通知栏的背景颜色不一致,我们需要对不同的背景做不同的适配才能保证上面的文字能够正常显示,比方你写死了一个白色的文字,但是系统通知的背景也是白色,这样一来文字就不能正常显示了,当然如果使用系统的通知样式无法满足你的需求,只能使用自定义样式,可以参考这篇文章Android自定义通知样式适配。如果仅仅是为了兼容低版本我个人感觉完全没有必要,大家可以看看友盟指数,所以没有必要去做兼容。这个看不同的需求而定。
-
在更新通知时我做了判断
在更新通知时我做了判断看下代码,这里我设置只有当进度等于1时并且发生改变时做更新,因为progress的值可能为1.1,1.2,1.3,但是显示的都会是1%,如果我不做判断限制那么每次onLoading的回调都会更新通知内容,也就是当1.1时会更新,1.2时也会执行更新的方法,但是前台展示的都是1%,所以做了好多无用功,并且当你不做限制时整个app会非常的卡,有时会卡死。这个你可以不做限制自己运行下Demo试试,绝壁卡死。
/** * 更新通知 */ public void updateNotification(long progress) { int currProgress = (int) progress; if (preProgress < currProgress) { builder.setContentText(progress + "%"); builder.setProgress(100, (int) progress, false); notificationManager.notify(NOTIFY_ID, builder.build()); } preProgress = (int) progress; }
总结
最后我还是希望大家如果有时间可以自己跟着我的步骤敲敲代码,看看那些我推荐的文章,我相信大家肯定能收获好多,大家有什么好的建议或者我哪里写的有问题的希望能够及时给我反馈。如果这篇文章对你有帮助的话欢迎大家多多的关注我,支持我,别忘了到github上star和fork我哈,源码DownLoadManager