使用Retrofit+RxJava下载文件并实现APP更新

    • 前言
    • 所需环境
      • 后台接口
      • Gradle配置
      • 权限设置
    • Retrofit和RxJava类与方法
      • service 接口定义
      • DownloadUtils
      • 拦截器
      • 下载监听回调
      • 下载请求体
    • MVP下的使用逻辑
      • Contract
      • Activty
      • presenter
    • 注意
    • 参考资料

前言

早在去年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文件

<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 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() {

                    @Override
                    public InputStream call(ResponseBody responseBody) {
                        return responseBody.byteStream();
                    }
                })
                .observeOn(Schedulers.computation()) // 用于计算任务
                .doOnNext(new Action1() {
                    @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() {
            @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文件存储—内部存储,外部存储以及各种存储路径解惑

你可能感兴趣的:(Android开发记录)