前言
早在去年8月的时候学习OkHttp的使用写了这篇《通过okhttp3下载文件实现APP版本更新》,一年过去了,也没多大的长进。凑巧最近又要实现app更新功能,将之前的文章翻出来看一下,添加了Retrofit+RxJava的使用,记录一下,以便以后查阅。
所需环境
后台接口
这次就不能再像上一年那样通过一个txt文件来存储apk信息了,我们要做的就是请后台吃顿饭,写一下以下接口
- 上传接口putApk
参数名 | 类型 | 含义 | 是否必选 |
---|---|---|---|
version | String | 版本号 | 是 |
Description | String | 描述 | 是 |
file | file | apk文件 | 是 |
这个接口用于方便我们上传新版本,可暂时配合postman使用
- 获取apk接口 getApk
参数名 | 类型 | 含义 | 是否必选 |
---|---|---|---|
version | String | 版本号 | 是 |
Description | String | 描述 | 是 |
url | String | apk下载地址 | 是 |
我们通过当前版本号和version的对比判断是否需要更新
Gradle配置
//retrofit
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'io.reactivex:rxandroid:1.1.0'//处理网络请求在android中线程调度问题
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'//gson转换
implementation 'com.squareup.retrofit2:adapter-rxjava:2.4.0'
implementation 'com.trello.rxlifecycle2:rxlifecycle:2.2.1'//解决RxJava内存泄漏
implementation 'com.trello.rxlifecycle2:rxlifecycle-components:2.2.1'
implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'//使用拦截器
在配置的时候要注意使用拦截器的版本要和retrofit使用的okhttp3的版本保持一致,否则容易出现java.lang.IllegalStateException: Fatal Exception thrown on Scheduler.Worker thread异常
权限设置
- 添加读写,网络权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET"/>
- 在application内添加
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="项目包名.fileprovider"
android:grantUriPermissions="true"
android:exported="false"
>
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
- 在res中新建xml资源文件夹并创建file_paths文件
<?xml version="1.0" encoding="utf-8"?>
<paths>
<!--外部存储路径-->
<external-path path="Android/data/com.nongyan.xinzhihouse/" name="files_root" />
<!--内部存储路径-->
<files-path
name="Android/data/com.nongyan.xinzhihouse/"
path="files_root">
</files-path>
</paths>
这两步是因为Android 7.0 以上google引入私有目录被限制访问和StrictMode API,也就是说在 /Android
/data我们是有权限访问的,但接下的文件我们就需要授权申请了
Retrofit和RxJava类与方法
该模块内容参考https://blog.csdn.net/jiashuai94/article/details/78775314
service 接口定义
public interface Service {
@Streaming
@GET
Observable<ResponseBody> download(@Url String url);
}
DownloadUtils
public class DownloadUtils{
private static final String TAG = "DownloadUtils";
private static final int DEFAULT_TIMEOUT = 15;
private Retrofit retrofit;
private JsDownloadListener listener;
private String baseUrl;
private String downloadUrl;
private RetrofitHelper retrofitHelper ;
public DownloadUtils(String baseUrl, JsDownloadListener listener) {
this.baseUrl = baseUrl;
this.listener = listener;
JsDownloadInterceptor mInterceptor = new JsDownloadInterceptor(listener);
OkHttpClient httpClient = new OkHttpClient.Builder()
.addInterceptor(mInterceptor)
.retryOnConnectionFailure(true)
.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
.build();
retrofit = new Retrofit.Builder()
.baseUrl(baseUrl)
.client(httpClient)
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
}
/**
* 开始下载
* @param url
* @param file
* @param subscriber
*/
public void download(@NonNull String url, final File file, Subscriber subscriber) {
retrofit.create(Service.class)
.download(url)
.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.map(new Func1<ResponseBody, InputStream>() {
@Override
public InputStream call(ResponseBody responseBody) {
return responseBody.byteStream();
}
})
.observeOn(Schedulers.computation()) // 用于计算任务
.doOnNext(new Action1<InputStream>() {
@Override
public void call(InputStream inputStream) {
writeFile(inputStream, file);
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(subscriber);
}
/**
* 将输入流写入文件
* @param inputString
* @param file
*/
private void writeFile(InputStream inputString, File file) {
if (file.exists()) {
file.delete();
}
FileOutputStream fos = null;
try {
fos = new FileOutputStream(file);
byte[] b = new byte[1024];
int len;
while ((len = inputString.read(b)) != -1) {
fos.write(b,0,len);
}
inputString.close();
fos.close();
} catch (FileNotFoundException e) {
listener.onFail("FileNotFoundException");
} catch (IOException e) {
listener.onFail("IOException");
}
}
}
拦截器
public class JsDownloadInterceptor implements Interceptor {
private JsDownloadListener downloadListener;
public JsDownloadInterceptor(JsDownloadListener downloadListener) {
this.downloadListener = downloadListener;
}
@Override
public Response intercept(Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
return response.newBuilder().body(
new JsResponseBody(response.body(), downloadListener)).build();
}
}
下载监听回调
public interface JsDownloadListener {
void onStartDownload(long length);
void onProgress(int progress);
void onFail(String errorInfo);
}
下载请求体
public class JsResponseBody extends ResponseBody {
private ResponseBody responseBody;
private JsDownloadListener downloadListener;
// BufferedSource 是okio库中的输入流,这里就当作inputStream来使用。
private BufferedSource bufferedSource;
public JsResponseBody(ResponseBody responseBody, JsDownloadListener downloadListener) {
this.responseBody = responseBody;
this.downloadListener = downloadListener;
downloadListener.onStartDownload(responseBody.contentLength());
}
@Override
public MediaType contentType() {
return responseBody.contentType();
}
@Override
public long contentLength() {
return responseBody.contentLength();
}
@Override
public BufferedSource source() {
if (bufferedSource == null) {
bufferedSource = Okio.buffer(source(responseBody.source()));
}
return bufferedSource;
}
private Source source(Source source) {
return new ForwardingSource(source) {
long totalBytesRead = 0L;
@Override
public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
totalBytesRead += bytesRead != -1 ? bytesRead : 0;
Log.e("download", "read: "+ (int) (totalBytesRead * 100 / responseBody.contentLength()));
if (null != downloadListener) {
if (bytesRead != -1) {
downloadListener.onProgress((int) (totalBytesRead));
}
}
return bytesRead;
}
};
}
}
MVP下的使用逻辑
我使用的Demo是采用mvp模式写的,所以以下逻辑需要用mvp模式视角来处理
Contract
public interface Contract {
interface View
{
void showError(String s);
void showUpdate(UpdateInfo updateInfo);
void downLoading(int i);
void downSuccess();
void downFial();
void setMax(long l);
}
interface Presenter{
void getApkInfo();
void downFile(String url);
}
}
Activty
在用户activity中需要处理一下操作
- 唤起更新apk请求
private void updateApk() {
if (Build.VERSION.SDK_INT >= 23) {//如果是6.0以上的
int REQUEST_CODE_CONTACT = 101;
String[] permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE};
//验证是否许可权限
for (String str : permissions) {
if (MainActivity.this.checkSelfPermission(str) != PackageManager.PERMISSION_GRANTED) {
//申请权限
MainActivity.this.requestPermissions(permissions, REQUEST_CODE_CONTACT);
return;
}
}
}
presenter.getApkInfo();
}
处理版本信息,决定是否更新
@Override public void showUpdate(final UpdateInfo updateInfo) { try { PackageManager packageManager = this.getPackageManager(); PackageInfo packageInfo = packageManager.getPackageInfo(this.getPackageName(),0); now_version = packageInfo.versionCode;//获取原版本号 } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } if(now_version== updateInfo.getVersion()){ Toast.makeText(this, "已经是最新版本", Toast.LENGTH_SHORT).show(); Log.d("版本号是", "onResponse: "+now_version); }else{ AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); builder.setIcon(android.R.drawable.ic_dialog_info); builder.setTitle("请升级APP至版本" + updateInfo.getVersion()); builder.setMessage(updateInfo.getDescription()); builder.setCancelable(false); builder.setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Log.e("MainActivity",String.valueOf(Environment.MEDIA_MOUNTED)); downFile(updateInfo.getUrl()); } }); builder.setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { } }); builder.create().show(); } }
开始更新,设置进度条
//下载apk操作 public void downFile(final String url) { progressDialog = new ProgressDialog(MainActivity.this); //进度条,在下载的时候实时更新进度,提高用户友好度 progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); progressDialog.setTitle("正在下载"); progressDialog.setMessage("请稍候..."); progressDialog.setProgress(0); progressDialog.show(); File file = new File(getApkPath(),"ZhouzhiHouse.apk"); //获取文件路径 presenter.downFile(url,file); Log.d("SettingActivity", "downFile: "); } //文件路径 public String getApkPath() { String directoryPath=""; if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ) {//判断外部存储是否可用 directoryPath =getExternalFilesDir("apk").getAbsolutePath(); }else{//没外部存储就使用内部存储 directoryPath=getFilesDir()+File.separator+"apk"; } File file = new File(directoryPath); Log.e("测试路径",directoryPath); if(!file.exists()){//判断文件目录是否存在 file.mkdirs(); } return directoryPath; }
- 设置进度条大小
@Override
public void setMax(final long total) {
progressDialog.setMax((int) total);
}
更新进度条
/** * 进度条实时更新 * @param i */ @Override public void downLoading(final int i) { progressDialog.setProgress(i); }
更新完成,唤起安装界面
/** * 下载成功 */ @Override public void downSuccess() { if (progressDialog != null && progressDialog.isShowing()) { progressDialog.dismiss(); } AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); builder.setIcon(android.R.drawable.ic_dialog_info); builder.setTitle("下载完成"); builder.setMessage("是否安装"); builder.setCancelable(false); builder.setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Intent intent = new Intent(Intent.ACTION_VIEW); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //android N的权限问题 intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//授权读权限 Uri contentUri = FileProvider.getUriForFile(MainActivity.this, "com.nongyan.xinzhihouse.fileprovider", new File(getApkPath(), "ZhouzhiHouse.apk"));//注意修改 intent.setDataAndType(contentUri, "application/vnd.android.package-archive"); } else { intent.setDataAndType(Uri.fromFile(new File(getApkPath(), "ZhouzhiHouse.apk")), "application/vnd.android.package-archive"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } startActivity(intent); } }); builder.setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { } }); builder.create().show(); }
presenter
- 获取最新apk信息
这里我使用的model和下载的model不是同一个,需要自己编写,所用接口就是上面的下载apk信息接口getApk,
需要这部分的资料可以看基于OkHttp3的Retrofit使用实践,里面的例子足以完成Retrofit的网络请求
@Override
public void getApkInfo() {
RetrofitModel retrofitModel = new RetrofitModel();
retrofitModel.getApkInfo(new MainListener<UpdateInfo>() {
@Override
public void onSuccess(UpdateInfo updateInfo) {
view.showUpdate(updateInfo);
}
@Override
public void onfail(String s) {
view.showError(s);
}
});
}
- 下载文件
@Override
public void downFile(String url) {
final DownloadUtils downloadUtils = new DownloadUtils(Api.BASE_URL, new JsDownloadListener() {
@Override
public void onStartDownload(long length) {
view.setMax(length);
}
@Override
public void onProgress(int progress) {
view.downLoading(progress);
}
@Override
public void onFinishDownload() {
view.downSuccess();
}
@Override
public void onFail(String errorInfo) {
view.showError(errorInfo);
}
});
File file = new File(view.getApkPath(),"ZhouzhiHouse.apk");
downloadUtils.download(url, file, new Subscriber() {
@Override
public void onCompleted() {
view.downSuccess();
}
@Override
public void onError(Throwable e) {
view.showError("onError:"+e);
}
@Override
public void onNext(Object o) {
}
});
}
注意
- 引入依赖版本的是否一致
- android 不同版本的处理
- 文件的路径
- 在build.gradle中versionCode面向开发者,versionName面向用户