[TOC]
效果图
网速较慢,只录制了最后几秒,效果还是看得出来。
需求
实现app的版本自动更新。
思路
其实Google官方专门提供了一个DownloadManager类来实现app的自动更新功能,因为项目的网络框架用的Retrofit2配合Rxjava2,因此想试一下自己去实现,顺便练练手。
此处下载大文件,肯定是要放在后台操作,放在服务中比较合适,而这个服务唯一的任务就是下载,因此我们用IntentService,然后在服务中通过发送Notification实现下载进度的实时监控,那么问题来了,下载过程中如何得到当前的下载进度?这个应该就是难点所在了,本来是想在写入数据到文件的时候将写入的长度记录下来,后来意外在网上看到一种更优雅的方式,通过自定义OkHttp网络拦截器的方式来获取到当前已经下载的数据长度,并通过Rxbus发布数据(参考链接),在此做个记录。
实现
自定义ResponseBody
/**
* 下载文件
* Created by lxf on 2017/3/3.
*/
public class FileResponseBody extends ResponseBody {
private Response originalResponse;//原结果
public FileResponseBody(Response originalResponse) {
this.originalResponse = originalResponse;
}
//返回内容类型
@Override
public MediaType contentType() {
return originalResponse.body().contentType();
}
//返回内容长度,没有则返回-1
@Override
public long contentLength() {
return originalResponse.body().contentLength();
}
//返回缓存源,类似于io中的BufferedReader
@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().send(new DownloadBean(contentLength(), bytesReaded));
return bytesRead;
}
});
}
}
上面的代码已经加了注解,如果对Okio不了解的同学可以参考下这里。
配置网络接口
由于是通过Retrofit2下载文件,返回的Obserable
interface Api {
...
@Streaming//注明为流文件,防止retrofit将大文件读入内存
@GET
Observable down(@Url String url);//通过@Url覆盖baseurl
}
实现网络接口:
public Observable down(String url) {
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(NetConfig.CONNECT_TIME_OUT, TimeUnit.MILLISECONDS)
.addNetworkInterceptor(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();
}
})
.build();
Retrofit retrofit = new Retrofit.Builder()
.client(client)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
Api api = retrofit.create(Api.class);
return api.down(url);
}
封装UpdateManager
public class UpdateManager {
/**
* 是否需要更新,需要则下载
*
* @param context 上下文
* @param url 新版本地址
* @param apkPath 本地apk保存路径
* @param cd 订阅关系集合,在数据传输完毕时解除订阅
*/
public static void downloadApk(final Context context, final String url, final String apkPath, final CompositeDisposable cd) {
NetWork.getInstance().down(url)
.map(new Function() {
@Override
public BufferedSource apply(ResponseBody responseBody) throws Exception {
return responseBody.source();//获取数据缓冲源
}
})
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe(new Observer() {
@Override
public void onSubscribe(Disposable d) {
cd.add(d);
}
@Override
public void onNext(BufferedSource bufferedSource) {
try {
writeFile(bufferedSource, new File(apkPath));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onError(Throwable e) {
unSubscribe(cd);
}
@Override
public void onComplete() {
//安装apk
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(Uri.fromFile(new File(apkPath)), "application/vnd.android.package-archive");
context.startActivity(intent);
unSubscribe(cd);
}
});
}
/**
* 写入文件
*/
private static void writeFile(BufferedSource source, File file) throws IOException {
if (!file.getParentFile().exists())
file.getParentFile().mkdirs();
if (file.exists())
file.delete();
BufferedSink bufferedSink = Okio.buffer(Okio.sink(file));
bufferedSink.writeAll(source);
bufferedSink.close();
source.close();
}
/**
* 解除订阅
*
* @param cd 订阅关系集合
*/
private static void unSubscribe(CompositeDisposable cd) {
if (cd != null && !cd.isDisposed())
cd.dispose();
}
}
通过IntentService下载apk文件
public class MyIntentService extends IntentService {
private static final String ACTION_DOWNLOAD = "intentservice.action.download";
private static final String DOWNLOAD_URL = "downloadUrl";
private static final String APK_PATH = "apkPath";
private CompositeDisposable cd = new CompositeDisposable();
private NotificationCompat.Builder builder;
private NotificationManager notificationManager;
public MyIntentService() {
super("MyIntentService");
}
public static void startUpdateService(Context context, String url, String apkPath) {
Intent intent = new Intent(context, MyIntentService.class);
intent.setAction(ACTION_DOWNLOAD);
intent.putExtra(DOWNLOAD_URL, url);
intent.putExtra(APK_PATH, apkPath);
context.startService(intent);
}
@Override
protected void onHandleIntent(Intent intent) {
if (intent != null) {
String action = intent.getAction();
if (ACTION_DOWNLOAD.equals(action)) {
notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
builder = new NotificationCompat.Builder(this)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("开始下载")
.setAutoCancel(true)
.setContentText("版本更新");
notificationManager.notify(0, builder.build());
String url = intent.getStringExtra(DOWNLOAD_URL);
String apkPath = intent.getStringExtra(APK_PATH);
handleUpdate(url, apkPath);
}
}
}
private void handleUpdate(String url, String apkPath) {
subscribeEvent();//订阅下载进度
UpdateManager.downloadApk(this, url, apkPath, cd);
}
private void subscribeEvent() {
RxBus.getDefault().toObservable(DownloadBean.class)
.subscribe(new Observer() {
@Override
public void onSubscribe(Disposable d) {
cd.add(d);
}
@Override
public void onNext(DownloadBean downloadBean) {
int progress = (int) Math.round(downloadBean.getBytesReaded() / (double) downloadBean.getTotal() * 100);
builder.setContentInfo(String.valueOf(progress) + "%").setProgress(100, progress, false);
notificationManager.notify(0, builder.build());
if (progress == 100)
notificationManager.cancel(0);
}
@Override
public void onError(Throwable e) {
subscribeEvent();
}
@Override
public void onComplete() {
subscribeEvent();
}
});
}
@Override
public void onDestroy() {
super.onDestroy();
System.out.println("onDestory____MyIntentService");
}
}
调用方式
public class UpdateActivity extends BaseActivity {
...
public void startDownload(View view){
//要下载的文件地址
String url = "http://www.izis.cn/mygoedu/yztv_1.apk";
//下载后的保存路径
String apkPath = Environment.getExternalStorageDirectory().getPath() + "/yzkj.apk";
MyIntentService.startUpdateService(this,url,apkPath);
}
}
Demo地址
传送门