IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android实现App版本检测、下载与安装新版本apk -> 正文阅读

[移动开发]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

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-08-07 12:12:20  更:2021-08-07 12:13:51 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/17 13:52:30-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码