Android之版本更新

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文件里面声明


 
    
          // path为空代表的是整个文件目录
    

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


    

注意: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之版本更新)