后台接口
这次就不能再像上一年那样通过一个txt文件来存储apk信息了,我们要做的就是请后台吃顿饭,写一下以下接口
- 上传接口putApk
这个接口用于方便我们上传新版本,可暂时配合postman使用
- 获取apk接口 getApk
我们通过当前版本号和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;
}
- 设置进度条大小
/**
* 进度条实时更新
* @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面向用户
参考资料
使用Retrofit+RxJava实现带进度下载文件
Android7.0应用程序自助更新跳转安装界面出现解析包出错
彻底搞懂Android文件存储—内部存储,外部存储以及各种存储路径解惑