1. 程式人生 > >使用Retrofit+RxJava下載檔案並實現APP更新

使用Retrofit+RxJava下載檔案並實現APP更新

後臺介面

這次就不能再像上一年那樣通過一個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'//使用攔截器

許可權設定

  • 新增讀寫,網路許可權
    <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) {
            }
        });
    }