Retrofit2实现App自动更新

时间:2021-03-22 05:59:29

原理

  • 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进行简单封装

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;
}
});
}
}

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,同时发送通知在前台展示下载进度

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);
}
}

需要注意的几个地方

1.为什么在onStartCommand里面下载apk而不是onCreate里面

这个跟service的生命周期有关系,onStartCommand()在每次调用startService时候都会调用一次,而onCreate()方法只有在服务第一次启动的时候才会掉用。当一个服务已经开启了那么再次调用startService不会在调用onCreate()方法,而onStartCommand()会被再次调用。

2.发送通知设置进度用的是setProgress,这个方法只有在4.0以上才能用

如果不用这个方法想要兼容低版本,就只能使用自定义的Notification,然后自己创建一个含有ProgressBar的layout布局,但是自定义的Notification在不同的系统中的适配处理太麻烦了,因为不同的系统的通知栏的背景颜色不一致,我们需要对不同的背景做不同的适配才能保证上面的文字能够正常显示,比方你写死了一个白色的文字,但是系统通知的背景也是白色,这样一来文字就不能正常显示了,当然如果使用系统的通知样式无法满足你的需求,只能使用自定义样式,可以参考这篇文章Android自定义通知样式适配。如果仅仅是为了兼容低版本我个人感觉完全没有必要,大家可以看看友盟指数,所以没有必要去做兼容。这个看不同的需求而定。

3.在更新通知时我做了判断

在更新通知时我做了判断看下代码,这里我设置只有当进度等于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;
}

源码 DownLoadManager