Android实现App版本检测、下载与安装新版本apk

背景

很多Android应用都内置了新版本检测与在线更新功能,这个简单的功能主要包括检测、下载、安装三个环节,演示效果如下:
演示
下载完成以后,自动打开apk,跳到安装界面,交由用户操作:
安装

思路

想要实现上述功能,主要是分三个步骤来进行:

  1. App端向服务端发送网络请求,获取App的最新版本号信息,进行比较,如果服务端返回的版本号大于当前App的版本号,则开启第二步,下载新版本App;
  2. 有新版本App时,开启下载,并在界面上给出下载进度提示,增加交互性;
  3. 在下载达到100%的进度时,通过代码打开apk实现安装。

实现

1. 版本检测

版本检测就是通过发送网络请求至App的服务端,从服务端查询到最新版App的版本号是多少,一般来说,可以通过请求静态资源(手动配置文件等)或动态接口的方式来获取最新的版本号。

  • 静态资源的话主要就是在服务端放置一个可以被访问的配置文件,其中写明了最新的版本号是多少;
    动态接口的话就是服务端维护一个接口,可以返回版本号,好处就是可以与数据库结合,做一些更加复杂的操作,例如维护版本更新记录等。

在本篇文章里面,为了简单表达,我们使用第一种静态资源的方式,在服务端放置一个文本文件version,内容为JSON格式。其访问地址为http://host/app/version,访问后得到的内容形如:

{
	"versionCode": 1,
	"fileName" : "abc-20210806.apk"
}

其中,versionCode是最新版本App的versionCode(Android应用的配置属性),fileName是最新版App的文件名称,用来配合着做文件下载。

App端检测版本的代码:

RetrofitRequest.sendGetRequest(Constant.URL_APP_VERSION, new RetrofitRequest.ResultHandler(context) {
    ...
    @Override
    public void onResult(String response) {
        if (response == null || response.trim().length() == 0) {
            Toast.makeText(context, R.string.layout_version_no_new, Toast.LENGTH_SHORT).show();
            LoadingDialog.close();
            return;
        }
        try {
            JSONObject jsonObject = new JSONObject(response);
            if (!jsonObject.has("versionCode") || !jsonObject.has("fileName")) {
                Toast.makeText(context, R.string.layout_version_no_new, Toast.LENGTH_SHORT).show();
                LoadingDialog.close();
                return;
            }
            newVersionCode = jsonObject.getInt("versionCode");
            newFileName = jsonObject.getString("fileName");
            int versionCode = VersionUtil.getVersionCode(context);
            LoadingDialog.close();
            if (newVersionCode > versionCode) {
                showUpdateDialog(newFileName);
            } else {
                if (!isAutoCheck) {
                    Toast.makeText(context, R.string.layout_version_no_new, Toast.LENGTH_SHORT).show();
                }
            }
        } catch (JSONException e) {
            Toast.makeText(context, R.string.layout_version_no_new, Toast.LENGTH_SHORT).show();
            LoadingDialog.close();
        }
    }
...
});

其中newVersionCode > versionCode就是最服务器端的版本号与本App的版本进行比较的代码,根据比较结果,如果当前不是最新版本,则显示更新提醒对话框showUpdateDialog(newFileName)

private void showUpdateDialog(final String fileName) {
    ConfirmDialog dialog = new ConfirmDialog(context, new ConfirmDialog.OnClickListener() {
        @Override
        public void onConfirm() {
            showDownloadDialog(fileName);
        }
    });
    dialog.setTitle(R.string.note_confirm_title);
    dialog.setContent(R.string.layout_version_new);
    dialog.setConfirmText(R.string.layout_yes);
    dialog.setCancelText(R.string.layout_no);
    dialog.show();
}

2. 下载新版本apk

用户在更新对话框中点击“是”时,表示需要下载最新版apk,此时显示下载进度对话框,并启动下载,实时刷新下载进度:

private void showDownloadDialog(String fileName) {
    Builder builder = new Builder(context);

    View view = LayoutInflater.from(context).inflate(R.layout.dialog_download, null);
    proDownload = (ProgressBar) view.findViewById(R.id.pro_download);
    tvPercent = (TextView) view.findViewById(R.id.txt_percent);
    tvKbNow = (TextView) view.findViewById(R.id.txt_kb_now);
    tvKbAll = (TextView) view.findViewById(R.id.txt_kb_all);

    Button btnCancel = (Button) view.findViewById(R.id.btn_cancel);
    btnCancel.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if (downloadDialog != null) {
                downloadDialog.dismiss();
            }
            cancelUpdate = true;
        }
    });

    downloadDialog = builder.create();
    downloadDialog.setCanceledOnTouchOutside(false);
    downloadDialog.show();
    downloadDialog.getWindow().setContentView(view);

    downloadApk(fileName);
}

下载的后台线程和前端百分比更新动作:

private void downloadApk(String fileName) {
    ExecutorService executorService = Executors.newFixedThreadPool(1);

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(Constant.URL_CONTRACT_BASE)
            .callbackExecutor(executorService)
            .build();

    String url = String.format(Constant.URL_APP_DOWNLOAD, fileName);
    FileRequest fileRequest = retrofit.create(FileRequest.class);
    Call<ResponseBody> call = fileRequest.download(url);
    call.enqueue(new Callback<ResponseBody>() {
        @Override
        public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
            if (response.isSuccessful()) {
                if (writeResponseBodyToDisk(response.body())) {
                    downloadDialog.dismiss();
                } else {
                    mHandler.sendEmptyMessage(DOWNLOAD_ERROR);
                }
            } else {
                mHandler.sendEmptyMessage(DOWNLOAD_ERROR);
            }
        }

        @Override
        public void onFailure(Call<ResponseBody> call, Throwable t) {
            mHandler.sendEmptyMessage(DOWNLOAD_ERROR);
        }
    });
}

private boolean writeResponseBodyToDisk(ResponseBody body) {
    savePath = StorageUtil.getDownloadPath(context);
    File apkFile = new File(savePath, newFileName);
    InputStream inputStream = null;
    OutputStream outputStream = null;
    try {
        byte[] fileReader = new byte[4096];
        long fileSize = body.contentLength();
        long fileSizeDownloaded = 0;
        inputStream = body.byteStream();
        outputStream = new FileOutputStream(apkFile);

        BigDecimal bd1024 = new BigDecimal(1024);
        totalByte = new BigDecimal(fileSize).divide(bd1024, BigDecimal.ROUND_HALF_UP).setScale(0).intValue();

        while (!cancelUpdate) {
            int read = inputStream.read(fileReader);
            if (read == -1) {
                mHandler.sendEmptyMessage(DOWNLOAD_FINISH);
                break;
            }
            outputStream.write(fileReader, 0, read);
            fileSizeDownloaded += read;
            progress = (int) (((float) (fileSizeDownloaded * 100.0 / fileSize)));
            downByte = new BigDecimal(fileSizeDownloaded).divide(bd1024, BigDecimal.ROUND_HALF_UP).setScale(0).intValue();
            mHandler.sendEmptyMessage(DOWNLOAD_ING);
        }
        outputStream.flush();
        return true;
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    } finally {
        if (outputStream != null) {
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

private void showProgress() {
    proDownload.setProgress(progress);
    tvPercent.setText(progress + "%");
    tvKbAll.setText(totalByte + "Kb");
    tvKbNow.setText(downByte + "Kb");
}

3. 安装apk

最新版本的apk下载完成后,调用安装代码执行安装动作。新旧版Android SDK安装方式略有区别,详见代码:

private void installApk() {
    File apkFile = new File(savePath, newFileName);
    if (!apkFile.exists()) {
        return;
    }
    Intent intent = new Intent(Intent.ACTION_VIEW);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        Uri contentUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", apkFile);
        intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
    } else {
        intent.setDataAndType(Uri.parse("file://" + apkFile.toString()), "application/vnd.android.package-archive");
    }
    context.startActivity(intent);
}

总结

Android端的版本更新相对比较自由,不受应用商店的限制。实现起来思路清晰,各环节一步步走下来还算简单,只是这其中有几点需要开发者注意:

  1. 这个的版本号versionCode对应的是build.gradle中的versionCode不是versionName,Android系统也是根据versionCode来确定安装的应用是否为新版本;
  2. 想要在进度条中准确显示下载进度的话,App在下载时应能够读取到apk的大小,如果apk是以静态资源形式提供的,还比较方便,一般从web服务器上都能够读到,如上述的代码body.contentLength()。如果是通过从服务端的文件流接口返回的话,一定要让文件流接口正确返回Http请求的Content-Length属性,否则无法读取到apk的大小,就无法准确的表达进度了。
  3. 上述的演示操作是用户主动更新,如果想要做后台无交互的自动更新,则只需要修改一个构造参数,使用new UpdateManager(this, UpdateManager.CHECK_AUTO).checkUpdate()即可,检测过程不会有loading效果。
  4. 动态权限申请、Dialog定制等不是本文的重点,但源码完整可用,包含这部分内容。
  5. 服务端版本配置文件和下载程序代码,都放置在源码的versionConfig文件夹内,仅供参考。

源码下载

见:http://github.com/ahuyangdong/VersionDownload

  • 3
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值