Android之版本更新

作者: 乌托邦式的爱情 | 来源:发表于2021-08-10 16:39 被阅读0次

    android中的版本更新是每一个APP的标配,记得最早的时候还是使用HttpUrlConnection+Handler来实现,如今时过境迁,特别是随着OkHttp、RxJava的流行,HttpUrlConnection+Handler的慢慢就用的少了,特别是在Android 7.0的手机上,系统对读取手机文件做了更进一步的限制,基于种种原因,最近又将版本更新做了一次整理,记录下来以防止后面遗忘。废话不多说,先上效果。

    版本更新之通知栏.gif

    这个效果是在点击立即更新之后,关闭AlertDialog,在后台下载,同时在通知栏显示下载进度(通知栏权限已打开)。

    版本更新之界面显示进度.gif

    这个效果是在点击立即更新之后,不关闭AlertDialog,直接显示下载进度。

    具体来说,大致的流程如下:


    版本更新流程图.png

    进入正文

    在正式编码之前,我们需要多方位思考,理清思路再编码,基于版本更新,首先我们需要知道几个点:
    (1)下载的时候必须要有进度通知,能够很清晰明了知道下载了多少。
    (2)有些时候我们需要对版本进行强制更新,所以这个功能必须有。
    (3)当用户下载完成后但是一不小心没有点安装,那么下次再次更新的时候应该是直接安装(APK必须要下载到本地),而不是再次从网络获取。
    (4)更新安装成功后最好能够自动删除原来的apk文件。

    当然,在实际的开发过程中,可能还有一些更为详细的需求,这个就根据实际情况来定了。了解需求之后,接下来我们看看应该怎么去编写。

    首先创建版本更新的类并声明相对应的变量
    /**
     * author: zhoufan
     * data: 2021/8/10 10:04
     * content: 实现App的版本更新
     */
    public class UpdateVersion {
    
        /**
         * 上下文
         */
        private Context mContext;
    
        /**
         * 版本号
         */
        private int mVersionCode;
    
        /**
         * 版本名称
         */
        private String mVersionName;
    
        /**
         * 更新提示内容
         */
        private String mUpdateContent;
    
        /**
         * 更新的APK对应的网络地址
         */
        private String mUpdateAPKUrl;
    
        /**
         * 更新的类型 1:正常更新,2:强制更新
         */
        private int mUpdateStatus = 1;
    
        /**
         * 更新的时候是否在前台开启进度条
         */
        private boolean isShowUpdateDialog;
    
        /**
         * 构造函数
         */
        public UpdateVersion(Context context, int versionCode, String versionName, String updateContent, String updateAPKUrl, int updateStatus, boolean isShowUpdateDialog) {
            this.mContext = context;
            this.mVersionCode = versionCode;
            this.mVersionName = versionName;
            this.mUpdateContent = updateContent;
            this.mUpdateAPKUrl = updateAPKUrl;
            this.mUpdateStatus = updateStatus;
            this.isShowUpdateDialog = isShowUpdateDialog;
        }
    }
    

    这里面声明了是否强制更新以及是否开启前台进度条,后面会使用到这两个参数。接下来就是我们的弹框了

    /**
     * 弹出Dialog显示更新对话框
     */
    private void createDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(mContext, R.style.bklistDialog);
        View updateView = LayoutInflater.from(mContext).inflate(R.layout.dialog_update_version, null);
        initDialogView(updateView);
        builder.setView(updateView);
        mDialog = builder.create();
        // 判断是否为强制更新
        if (mUpdateStatus == 2) {
            mDialog.setCanceledOnTouchOutside(false);
            mUpdateCancel.setVisibility(View.GONE);
            mDialog.setOnKeyListener(keyListener);
            mDialog.setCancelable(false);
        } else {
            mDialog.setCanceledOnTouchOutside(true);
        }
        // 显示
        mDialog.show();
    }
    
    /**
     * 获取View的控件并添加点击响应事件
     */
    private void initDialogView(View updateView) {
        TextView newVersionName = updateView.findViewById(R.id.new_version_value);
        TextView newVersionContent = updateView.findViewById(R.id.update_content);
        mUpdateProgressLayout = updateView.findViewById(R.id.update_download_layout);
        mUpdateProgressbar = updateView.findViewById(R.id.update_download_progressbar);
        mUpdateDownloadNumber = updateView.findViewById(R.id.update_download_number);
        mUpdateCancel = updateView.findViewById(R.id.cancel);
        mUpdateSure = updateView.findViewById(R.id.sure);
        String versionName = "V" + mVersionName;
        newVersionName.setText(versionName);
        if (!TextUtils.isEmpty(mUpdateContent)) {
            newVersionContent.setText(mUpdateContent);
        } else {
            newVersionContent.setText(mContext.getResources().getString(R.string.updateNewVersion));
        }
        mUpdateSure.setText(mContext.getResources().getString(R.string.new_update));
    }
    

    在这里我们就需要判断是否为强制更新了,因为强制更新的话就表示我们在AlertDialog弹出后无法被关闭,同时我们的取消按钮也要被隐藏掉。

    开始显示我们的AlertDialog
    public void showUpdateDialog() {
        // 设置下载的安装路径
        mSavePath = Environment.getExternalStorageDirectory().getPath() + File.separator + "Android/data/" + mContext.getPackageName() + File.separator + "apk";
        mSaveFileName = mSavePath + File.separator + mVersionCode + ".apk";
        createDialog();
        // 取消更新
        mUpdateCancel.setOnClickListener(v -> {
            if (mDialog != null && mDialog.isShowing())
                mDialog.dismiss();
        });
        // 确认更新
        mUpdateSure.setOnClickListener(v -> {
            File file = new File(mSaveFileName);
            if (file.exists()) {
                // apk文件已经存在,直接安装
                installApk(mSaveFileName);
            } else {
                // 后台开启下载功能,下载完毕后自动更新
                if (isShowUpdateDialog) {
                    mUpdateProgressLayout.setVisibility(View.VISIBLE);
                } else {
                    if (mDialog != null && mDialog.isShowing()) {
                        mDialog.dismiss();
                    }
                }
                startDownloadApk();
            }
        });
    }
    

    首先确定好我们的安装路径,这里我是放置在我的项目的根目录下,然后相应的处理取消和立即更新的事件,取消对应的比较简单,直接将弹框关闭就好,立即更新稍微复杂一点,分为几步:
    (1)首先判断要更新的apk文件存在不存在,如果存在的话直接安装apk文件即可。
    (2)如果不存在的话,需要判断是在通知栏显示下载进度还是在界面直接显示下载进度。
    (3)最后开启下载。

    开始下载
    /**
     * 使用OkHttp来进行网络下载
     */
    private void startDownloadApk() {
        OkHttpClient okHttpClient = new OkHttpClient();
        Request request = new Request.Builder().url(mUpdateAPKUrl).get().build();
        Call call = okHttpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                MyToast.showCenterSortToast(mContext, mContext.getString(R.string.download_fail));
            }
    
            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) {
                InputStream is = null;
                byte[] buf = new byte[2048];
                int len = 0;
                FileOutputStream fos = null;
                //储存下载文件的目录
                File dir = new File(mSavePath);
                if (!dir.exists()) {
                    dir.mkdirs();
                }
                File file = new File(mSaveFileName);
                try {
                    is = response.body().byteStream();
                    long total = response.body().contentLength();
                    fos = new FileOutputStream(file);
                    long sum = 0;
                    while ((len = is.read(buf)) != -1) {
                        fos.write(buf, 0, len);
                        sum += len;
                        int progress = (int) (sum * 1.0f / total * 100);
                        if (mUpdateProgressLayout.getVisibility() == View.VISIBLE) {
                            //下载中更新进度条
                            Observable.just(progress).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(integer -> {
                                mUpdateProgressbar.setProgress(integer);
                                String currentPercent = integer + "%";
                                mUpdateDownloadNumber.setText(currentPercent);
                            });
                        } else {
                            Observable.just(progress).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(integer -> {
                                updateNotification(progress);
                            });
                        }
                    }
                    fos.flush();
                    //下载完成
                    Observable.just(100).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(integer -> installApk(mSaveFileName));
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    try {
                        if (is != null) {
                            is.close();
                        }
                        if (fos != null) {
                            fos.close();
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
    }
    

    开启下载这一块我采用的就是最新的OkHttp+RxJava,这个就不用多说了,一看就懂。最后当下载完成之后就可以直接安装APK了。

    安装APK
    /**
     * 安装apk
     */
    private void installApk(String filePath) {
        if (mUpdateProgressLayout.getVisibility() == View.GONE) {
            NotificationManager manager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
            manager.cancel(1);
        }
        File file = new File(filePath);
        if (Build.VERSION.SDK_INT >= 24) {//判读版本是否在7.0以上
            String authority = mContext.getPackageName() + ".fileProvider";
            Uri apkUri = FileProvider.getUriForFile(mContext, authority, file);
            Intent install = new Intent(Intent.ACTION_VIEW);
            install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//添加这一句表示对目标应用临时授权该Uri所代表的文件
            install.setDataAndType(apkUri, "application/vnd.android.package-archive");
            mContext.startActivity(install);
        } else {
            Intent intent = new Intent(Intent.ACTION_VIEW);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intent.setDataAndType(Uri.fromFile(file),
                    "application/vnd.android.package-archive");
            mContext.startActivity(intent);
        }
    }
    

    安装APK稍微复杂一点,因为在android 7.0手机上面,系统做了一个更改。

    在应用间共享文件

    对于面向 Android 7.0 的应用,Android 框架执行的 StrictModeAPI 政策禁止在您的应用外部公开 file://URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。
    要在应用间共享文件,您应发送一项 content://URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider 类。如需了解有关权限和共享文件的详细信息,请参阅共享文件

    这是android 官方的解释,什么意思呢?简单来说就是在android 7.0手机上面,原理的Uri模式玩不转了,系统建议采用FileProvider来实现。那么FileProvider怎么使用呢?也很简单。

    FileProvider使用步骤

    (1)在res目录下新建xml文件夹,在xml文件夹下面新建一个provider_paths.xml文件。
    (2)在provider_paths.xml文件里面声明

    <?xml version="1.0" encoding="utf-8"?>
     <resources>
        <paths>
            <external-path
                name="apk"  // 名字随便取
                path="" />  // path为空代表的是整个文件目录
        </paths>
    </resources>
    

    (3)在清单文件里面进行声明

    <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="com.steven.sunworld.fileProvider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/provider_paths" />
    </provider>
    

    注意:authorities为包名+fileProvider,中间用 . 来连接,如上所示。
    (4)在使用FileProvider.getUriForFile(mContext, authority, file);的时候authority的取值必须和你在清单文件里面设置的一致,file就是你要操作的文件。

    到这里,版本更新的所有环节都基本结束了,最后贴上完整代码

    /**
     * author: zhoufan
     * data: 2021/8/10 10:04
     * content: 实现App的版本更新
     */
    public class UpdateVersion {
    
        /**
         * 上下文
         */
        private Context mContext;
    
        /**
         * 版本号
         */
        private int mVersionCode;
    
        /**
         * 版本名称
         */
        private String mVersionName;
    
        /**
         * 更新提示内容
         */
        private String mUpdateContent;
    
        /**
         * 更新的APK对应的网络地址
         */
        private String mUpdateAPKUrl;
    
        /**
         * 更新的类型 1:正常更新,2:强制更新
         */
        private int mUpdateStatus = 1;
    
        /**
         * 更新的时候是否在前台开启进度条
         */
        private boolean isShowUpdateDialog;
    
        /**
         * 更新弹出的确认更新确认框
         */
        private AlertDialog mDialog;
    
        /**
         * APK下载之后保存的地址
         */
        private String mSavePath;
    
        /**
         * 保存的文件名
         */
        private String mSaveFileName;
    
        /**
         * 当前下载的进度
         */
        private int mDownloadProgress;
    
        private RelativeLayout mUpdateProgressLayout;
        private ProgressBar mUpdateProgressbar;
        private TextView mUpdateDownloadNumber;
        private TextView mUpdateCancel;
        private TextView mUpdateSure;
    
        public UpdateVersion(Context context, int versionCode, String versionName, String updateContent, String updateAPKUrl, int updateStatus, boolean isShowUpdateDialog) {
            this.mContext = context;
            this.mVersionCode = versionCode;
            this.mVersionName = versionName;
            this.mUpdateContent = updateContent;
            this.mUpdateAPKUrl = updateAPKUrl;
            this.mUpdateStatus = updateStatus;
            this.isShowUpdateDialog = isShowUpdateDialog;
        }
    
        public void showUpdateDialog() {
            // 设置下载的安装路径
            mSavePath = Environment.getExternalStorageDirectory().getPath() + File.separator + "Android/data/" + mContext.getPackageName() + File.separator + "apk";
            mSaveFileName = mSavePath + File.separator + mVersionCode + ".apk";
            createDialog();
            // 取消更新
            mUpdateCancel.setOnClickListener(v -> {
                if (mDialog != null && mDialog.isShowing())
                    mDialog.dismiss();
            });
            // 确认更新
            mUpdateSure.setOnClickListener(v -> {
                File file = new File(mSaveFileName);
                if (file.exists()) {
                    // apk文件已经存在,直接安装
                    installApk(mSaveFileName);
                } else {
                    // 后台开启下载功能,下载完毕后自动更新
                    if (isShowUpdateDialog) {
                        mUpdateProgressLayout.setVisibility(View.VISIBLE);
                    } else {
                        if (mDialog != null && mDialog.isShowing()) {
                            mDialog.dismiss();
                        }
                    }
                    startDownloadApk();
                }
            });
        }
    
        /**
         * 弹出Dialog显示更新对话框
         */
        private void createDialog() {
            AlertDialog.Builder builder = new AlertDialog.Builder(mContext, R.style.bklistDialog);
            View updateView = LayoutInflater.from(mContext).inflate(R.layout.dialog_update_version, null);
            initDialogView(updateView);
            builder.setView(updateView);
            mDialog = builder.create();
            // 判断是否为强制更新
            if (mUpdateStatus == 2) {
                mDialog.setCanceledOnTouchOutside(false);
                mUpdateCancel.setVisibility(View.GONE);
                mDialog.setOnKeyListener(keyListener);
                mDialog.setCancelable(false);
            } else {
                mDialog.setCanceledOnTouchOutside(true);
            }
            // 显示
            mDialog.show();
        }
    
        /**
         * 获取View的控件并添加点击响应事件
         */
        private void initDialogView(View updateView) {
            TextView newVersionName = updateView.findViewById(R.id.new_version_value);
            TextView newVersionContent = updateView.findViewById(R.id.update_content);
            mUpdateProgressLayout = updateView.findViewById(R.id.update_download_layout);
            mUpdateProgressbar = updateView.findViewById(R.id.update_download_progressbar);
            mUpdateDownloadNumber = updateView.findViewById(R.id.update_download_number);
            mUpdateCancel = updateView.findViewById(R.id.cancel);
            mUpdateSure = updateView.findViewById(R.id.sure);
            String versionName = "V" + mVersionName;
            newVersionName.setText(versionName);
            if (!TextUtils.isEmpty(mUpdateContent)) {
                newVersionContent.setText(mUpdateContent);
            } else {
                newVersionContent.setText(mContext.getResources().getString(R.string.updateNewVersion));
            }
            mUpdateSure.setText(mContext.getResources().getString(R.string.new_update));
        }
    
        /**
         * 禁用返回键
         */
        private DialogInterface.OnKeyListener keyListener = (dialog, keyCode, event) -> keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0;
    
        /**
         * 安装apk
         */
        private void installApk(String filePath) {
            if (mUpdateProgressLayout.getVisibility() == View.GONE) {
                NotificationManager manager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
                manager.cancel(1);
            }
            File file = new File(filePath);
            if (Build.VERSION.SDK_INT >= 24) {//判读版本是否在7.0以上
                String authority = mContext.getPackageName() + ".fileProvider";
                Uri apkUri = FileProvider.getUriForFile(mContext, authority, file);
                Intent install = new Intent(Intent.ACTION_VIEW);
                install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//添加这一句表示对目标应用临时授权该Uri所代表的文件
                install.setDataAndType(apkUri, "application/vnd.android.package-archive");
                mContext.startActivity(install);
            } else {
                Intent intent = new Intent(Intent.ACTION_VIEW);
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                intent.setDataAndType(Uri.fromFile(file),
                        "application/vnd.android.package-archive");
                mContext.startActivity(intent);
            }
        }
    
        /**
         * 使用OkHttp来进行网络下载
         */
        private void startDownloadApk() {
            OkHttpClient okHttpClient = new OkHttpClient();
            Request request = new Request.Builder().url(mUpdateAPKUrl).get().build();
            Call call = okHttpClient.newCall(request);
            call.enqueue(new Callback() {
                @Override
                public void onFailure(@NotNull Call call, @NotNull IOException e) {
                    MyToast.showCenterSortToast(mContext, mContext.getString(R.string.download_fail));
                }
    
                @Override
                public void onResponse(@NotNull Call call, @NotNull Response response) {
                    InputStream is = null;
                    byte[] buf = new byte[2048];
                    int len = 0;
                    FileOutputStream fos = null;
                    //储存下载文件的目录
                    File dir = new File(mSavePath);
                    if (!dir.exists()) {
                        dir.mkdirs();
                    }
                    File file = new File(mSaveFileName);
                    try {
                        is = response.body().byteStream();
                        long total = response.body().contentLength();
                        fos = new FileOutputStream(file);
                        long sum = 0;
                        while ((len = is.read(buf)) != -1) {
                            fos.write(buf, 0, len);
                            sum += len;
                            int progress = (int) (sum * 1.0f / total * 100);
                            if (mUpdateProgressLayout.getVisibility() == View.VISIBLE) {
                                //下载中更新进度条
                                Observable.just(progress).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(integer -> {
                                    mUpdateProgressbar.setProgress(integer);
                                    String currentPercent = integer + "%";
                                    mUpdateDownloadNumber.setText(currentPercent);
                                });
                            } else {
                                Observable.just(progress).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(integer -> {
                                    updateNotification(progress);
                                });
                            }
                        }
                        fos.flush();
                        //下载完成
                        Observable.just(100).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(integer -> installApk(mSaveFileName));
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        try {
                            if (is != null) {
                                is.close();
                            }
                            if (fos != null) {
                                fos.close();
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
        }
    
        /**
         * 通知栏更新
         */
        private void updateNotification(int progress) {
            if (NotificationManagerCompat.from(mContext).areNotificationsEnabled()) {
                if (progress > mDownloadProgress) {
                    mDownloadProgress = progress;
                    NotificationManager manager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
                    if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
                        NotificationChannel notificationChannel = new NotificationChannel("1", "update", NotificationManager.IMPORTANCE_HIGH);
                        notificationChannel.setSound(null, null);
                        notificationChannel.enableLights(false);
                        notificationChannel.setLightColor(Color.RED);
                        notificationChannel.setShowBadge(false);
                        notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
                        manager.createNotificationChannel(notificationChannel);
                    }
                    // 创建自定义的样式布局
                    RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(), R.layout.download_progress_state_view);
                    // 在这里可以设置RemoteView的初始布局
                    NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, "1");
                    builder.setCustomContentView(remoteViews);
                    // 不可以自动取消
                    builder.setAutoCancel(false);
                    // 必须要设置,否则在android 10手机上面会闪退
                    builder.setSmallIcon(R.mipmap.ic_launcher_round);
                    // 设置通知的优先级
                    builder.setPriority(NotificationCompat.PRIORITY_MAX);
                    Notification nn = builder.build();
                    nn.contentView = remoteViews;
                    nn.icon = R.mipmap.ic_launcher;
                    remoteViews.setImageViewResource(R.id.download_progress_img, R.mipmap.ic_launcher);
                    String loadShow = mContext.getResources().getString(R.string.app_download_show);
                    remoteViews.setTextViewText(R.id.download_progress_name, loadShow);
                    remoteViews.setProgressBar(R.id.download_progressbar, 100, mDownloadProgress, false);
                    remoteViews.setTextViewText(R.id.download_progress_text, mDownloadProgress + "%");
                    manager.notify(1, nn);
                }
            }
        }
    }
    

    相关文章

      网友评论

        本文标题:Android之版本更新

        本文链接:https://www.haomeiwen.com/subject/exqfbltx.html