Android实现App内自动升级,适配了安卓7、8及以上版本

这篇具有很好参考价值的文章主要介绍了Android实现App内自动升级,适配了安卓7、8及以上版本。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

        应用发布后,要实现灰度升级控制,如果只依赖各家应用市场是不够的,还需要自己在应用中控制升级逻辑。并且每家应用市场上新审核也是一件很麻烦的事情,尤其像至简网格这样的应用,甚至没在应用市场上架,更不可能依赖它们了。所以必须要在应用中实现自动升级功能。

        网上有很多介绍,他们摸索的结果对我有很大帮助。可能是因为版本关系,或者关注点不同,照着做,会有很多过时的或错误的地方,所以我将摸索过程记录在此,防止忘记。

        下面几个图是在华为荣耀V9(安卓7.0、SDK 24)中的界面:

android app升级,android,至简网格,java,android,自动升级

图1、提醒有可升级的版本

android app升级,android,至简网格,java,android,自动升级

图2、下载版本

android app升级,android,至简网格,java,android,自动升级

图3、安卓7.0的安全检测界面 

     大致步骤如下:

  1. AndroidManifest及res设置;
  2. 申请外部存储读写权限;
  3. 申请安装应用;
  4. 向服务端查询是否有可升级版本,下载版本,执行安装;

       安卓各个版本差异较大,我的测试日期为(2023.5.29),测试环境为小米8(安卓10、SDK29)、华为荣耀v9(安卓7.0、SDK 24)两种。因为不考虑兼容安卓7之前的版本,所以代码中也无相关实现。

一、AndroidManifest及res设置

1、AndroidManifest设置

增加以下权限:

<!--  网络权限,不用在程序中动态申请 -->
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<!--  外部存储读写,需要在程序中动态申请,用于存储运行日志,以及下载的升级版本-->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 安装APK权限,需要在程序中动态申请,并且不同于外部存储读写权限申请 -->
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

    <application
        android:name="cn.net.zhijian.mesh.client.MeshClientApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:hardwareAccelerated="true"
        android:usesCleartextTraffic="true"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MeshClient"
        android:requestLegacyExternalStorage="true"
        android:windowSoftInputMode="stateHidden|adjustResize">

......
        <!-- fileprovider名称在安装时传递给系统安装程序 -->
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/autoupdate" />
        </provider>
    </application>

有以下几点需要注意:

  • application中需要增加属性android:requestLegacyExternalStorage="true";
    
  • provider属性android:authorities="${applicationId}.fileprovider",这个名称可以自己定,但是在执行安装时必须保持一致,后面会再次提到;
     
  • provider中meta-data->android:resource="@xml/autoupdate"名称可以自己定,但是需确保在res/xml/下有同名的xml文件,Android7.0及以上版本需要通过FileProvider方式进行安装,文件内容见下一节;
    

    android app升级,android,至简网格,java,android,自动升级

    2、res中的准备

  • 在res中新建一个xml目录,创建autoupdate.xml,内容如下,注意其中的注释;
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 如果不设置root,将会发生“Failed to find configured root that contains...”错误 -->
    <root-path name="root_path" path="."/>
<!-- name与path,好像并无太多限制,请了解的同学指正以下 -->
    <external-path name="autoupdate" path="download/" />
</paths>
  • 下载安装界面定义

        在res/layout中增加download_dlg.xml,用以显示下载进度及安装中碰到的问题,怎样显示请看后面的Updater类实现。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="2mm"
    android:orientation="vertical">
    <ProgressBar
        android:id="@+id/progress"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <TextView
        android:id="@+id/txtMsg"
        android:layout_margin="2mm"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text=""
        android:textSize="16sp"
        android:textStyle="normal" />
</LinearLayout>

      

二、申请外部存储读写权限与安装权限

        安卓5.0之后,申请权限的操作变化很大,使用起来比老版本好一些,同一个逻辑不会被打散到多个地方实现。以下实现在MainActivity.onCreate中调用,没有考虑兼容老版本。里面有个updater.checkVersion调用,后面会讲到,用来查询服务端是否有新版本,可以根据自己的需要做不同的实现。因为只有在具备外部存储读写权限后才可以执行升级操作,所以申请成功后,才会检查是否有可升级版本。如果没有新版本,是不会出现申请安装应用权限的界面,否则系统的提示会吓退一部分用户。

         申请安装权限的实现与申请外部存储读写权限不同,在安卓8.0(SDK26)后有一次大变动。在后面的Updater类中,如果是8.0之前的版本则直接安装,否则要申请权限。

        //申请必要的权限
        Updater updater = new Updater(this);
        /*
         * 申请安装应用的权限。
         * registerForActivityResult必须在onCreate中调用,
         * 否则会报错:LifecycleOwners must call register before they are STARTED.
         */
        ActivityResultLauncher<Intent> installApkLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
        (result) -> {//安装申请确认完毕后的回调
            if (result.getResultCode() == Activity.RESULT_OK) {
                updater.showDownloadDialog();
            }
        });

        /*
         * 申请外部存储卡读写权限。
         * 调用Environment.getExternalStoragePublicDirectory等函数,必须具备外部存储读写权限,
         * 除了在manifest中要声明权限,同时在application中设置android:requestLegacyExternalStorage="true"
         * 并且,还需要在代码中动态申请。
         * 申请成功后才能确定应用升级可以执行下去,所以才会查询新版本。
         */
        registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(),
            result -> {//权限申请执行完毕后的回调
                String permissions = "";
                boolean allPassed = true;
                for (Map.Entry<String, Boolean> p : result.entrySet()) {
                    permissions += p.getKey() + ':' + p.getValue() + '\n';
                    if(!p.getValue()) {
                        allPassed = false;
                    }
                }
                LOG.debug("STORAGE_PERMISSION grantResults:\n{}", permissions);
                if(allPassed) { //有了外部存储读写权限之后再判断是否有升级版本
                    updater.checkVersion(installApkLauncher);
                }
            }
        ).launch(new String[] {
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.READ_EXTERNAL_STORAGE
        });

      

三、升级安装

        检查版本、下载、安装,都在Updater类中实现,在申请外部存储读写权限、申请安装权限时,会调用到Updater中的函数。

        代码中出现的cn.net.zhijian包下的类都是我的公共类,看的时候可以忽略,根据函数名称应该能大致猜出它的功能。

        注意其中的String authority = BuildConfig.APPLICATION_ID + ".fileprovider";前面提到过,必须与provider定义中保持一致。否则会提示Couldn't find meta-data for provider with authority...错误。

     installApk(File apkFile, String digest, AbsHttpCallback.IDownloadProgress progress)

  1. apkFile:已下载的安装文件,我指定的路径是context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)+"/app.apk",似乎autoupdate.xml中的设置在此并未起什么作用;
  2. digest:从我的服务器上查到的文件md5值,安装前比较校验码,不同则拒绝安装;
  3. progress:用以提示下载进度、安装错误信息等;
  showUpdateDialog(ActivityResultLauncher<Intent> installPermApply)

      installPermApply是在MainActivity.onCreate中初始化安装权限申请加载器时传递进来的。安卓8.0及以上版本才会调用它,其他情况则直接显示下载安装界面。

import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.Settings;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;

import androidx.activity.result.ActivityResultLauncher;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.FileProvider;

import org.slf4j.Logger;

import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import cn.net.zhijian.mesh.client.abs.AbsHttpCallback;
import cn.net.zhijian.mesh.client.abs.IConst;
import cn.net.zhijian.mesh.client.abs.IThreadPool;
import cn.net.zhijian.mesh.client.bean.Company;
import cn.net.zhijian.mesh.client.bean.RequestOptions;
import cn.net.zhijian.mesh.client.util.HttpClient;
import cn.net.zhijian.meshclient.BuildConfig;
import cn.net.zhijian.meshclient.R;
import cn.net.zhijian.util.FileUtil;
import cn.net.zhijian.util.HttpUtil;
import cn.net.zhijian.util.LogUtil;
import cn.net.zhijian.util.StringUtil;
import cn.net.zhijian.util.UrlPathInfo;
import cn.net.zhijian.util.ValParser;

class Updater {
    private static final Logger LOG = LogUtil.getInstance();

    private final Activity context;

    private String verFromSrv; //服务端返回的应用版本号
    private String cdnUrl; //服务端返回的CDN头部地址,后面加上/app_id/version/app.apk
    private String digest; //服务端返回的应用apk校验码
    private int size; //服务端返回的应用apk大小
    private List<String> features; //服务端返回的新版本的特性列表

    public Updater(Activity context) {
        this.context = context;
    }

    private void showUpdateDialog(ActivityResultLauncher<Intent> installPermApply) {
        AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setTitle(R.string.there_is_new_ver);
        builder.setIcon(R.drawable.download);
        StringBuilder sb = new StringBuilder();
        sb.append(context.getString(R.string.ver_no)).append(this.verFromSrv).append('\n');
        for(String f : features) {
            sb.append(f).append('\n');
        }

        builder.setMessage(sb.toString());
        builder.setPositiveButton(R.string.update_rightnow, (DialogInterface dialog, int which) -> {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                boolean haveInstallPermission = context.getPackageManager().canRequestPackageInstalls();
                if (!haveInstallPermission) { //如果已经有权限,不必再申请
                    Uri packageURI = Uri.parse("package:" + BuildConfig.APPLICATION_ID);
                    Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, packageURI);
                    installPermApply.launch(intent); //权限申请通过后执行showDownloadDialog
                    return;
                }
            }
            showDownloadDialog();// 版本<26(Android 8)或已申请了权限,则直接显示下载安装
        });
        builder.setNegativeButton(R.string.do_it_later, (DialogInterface dialog, int which) -> {
            dialog.dismiss();
        });

        builder.create().show();
    }

    /**
     * 显示下载对话框,在其中显示下载、安装的进度,
     * 如果发生错误,也会显示错误信息
     */
    public void showDownloadDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setTitle(R.string.update_apk);
        LayoutInflater inflater = LayoutInflater.from(context);
        View v = inflater.inflate(R.layout.download_dlg, null);
        ProgressBar progressBar = (ProgressBar) v.findViewById(R.id.progress);
        TextView txtMsg = v.findViewById(R.id.txtMsg);

        builder.setView(v);
        builder.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {
            //canceled = true;
        });

        //用于显示当前的进度,请参照download_dlg.xml中的UI定义
        AbsHttpCallback.IDownloadProgress progress = new AbsHttpCallback.IDownloadProgress() {
            String header = "";

            @Override
            public void progress(int curSize) {
                int percent = (int) (((float) curSize / size) * 100);
                context.runOnUiThread(() -> {
                    txtMsg.setText(header + percent + "%");
                    progressBar.setProgress(percent);
                });
            }

            @Override
            public void message(String msg) {
                this.header = msg;
                context.runOnUiThread(() -> {
                    txtMsg.setText(header);
                });
            }
        };

        builder.create().show();
        String url = cdnUrl;
        if(!url.endsWith("/")) {
            url += '/';
        }
        url += BuildConfig.APPLICATION_ID + '/' + this.verFromSrv + "/app.apk";

        String saveAs = FileUtil.addPath(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "app.apk");
        File f = new File(saveAs);
        if(f.exists()) { //如果文件存在,并且校验码相同,则不必再次下载
            String localDigest = FileUtil.digest(f);
            if(digest.equals(localDigest)) {
                LOG.info("Reinstall apk {}, size:{}", saveAs, size);
                context.runOnUiThread(() -> {
                    progress.progress(size);
                    installAPK(new File(saveAs), digest, progress);
                });
                return;
            }
        }

        progress.message(context.getString(R.string.downloading));
        HttpClient.download(url, saveAs, progress).whenCompleteAsync((hr, e) -> {
            if(e != null) {
                LOG.error("Fail to download {}", cdnUrl, e);
                return;
            }

            if(hr.code != RetCode.OK || hr.data == null || hr.data.size() == 0) {
                LOG.error("Fail to download {}, result:{}", cdnUrl, hr.brief());
                return;
            }
            int appSize = ValParser.getAsInt(hr.data, "size");
            if(appSize != size) {
                LOG.error("Fail to download {}, invalid size({}!={}}", cdnUrl, size, appSize);
                return;
            }
            LOG.info("Reinstall apk {}, size:{}", ValParser.getAsStr(hr.data, "saveAs"), size);
            context.runOnUiThread(() -> {
                installAPK(new File(saveAs), digest, progress);
            });
        });
    }

    /**
     * 安装apk
     * @param apkFile apk文件完整路径
     * @param digest 校验码
     * @param progress 打印消息的回调
     */
    private void installAPK(File apkFile, String digest, AbsHttpCallback.IDownloadProgress progress) {
        progress.message(context.getString(R.string.installing));
        try {
            if (!apkFile.exists()) {
                LOG.error("Update apk file `{}` not exists", apkFile);
                progress.message(context.getString(R.string.apk_not_exists));
                return;
            }

            String localDigest = FileUtil.digest(apkFile);
            if(!localDigest.equals(digest)) {
                LOG.error("Invalid apk file `{}` digest({}!={})", apkFile, localDigest, digest);
                progress.message(context.getString(R.string.wrong_digest));
                return;
            }

            Intent intent = new Intent(Intent.ACTION_VIEW);
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//安装完成后打开新版本
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 给目标应用一个临时授权
            //Build.VERSION.SDK_INT >= 24,使用FileProvider兼容安装apk
            //packageName也可以通过context.getApplicationContext().getPackageName()获取
            String authority = BuildConfig.APPLICATION_ID + ".fileprovider";
            Uri apkUri = FileProvider.getUriForFile(context, authority, apkFile);
            intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
            context.startActivity(intent);
            //安装完之后会提示”完成” “打开”。
            android.os.Process.killProcess(android.os.Process.myPid());
        } catch (Exception e) {
            LOG.error("Fail to install apk {}", apkFile, e);
            progress.message(context.getString(R.string.fail_to_install));
        }
    }

    public void checkVersion(ActivityResultLauncher<Intent> installPermApply) {
        Company company = RequestOptions.getCompany(Company.PERSONAL_COMPANY_ID);
        int localVer = StringUtil.verToInt(IConst.VERSION);
        UrlPathInfo url = new UrlPathInfo("/checkAppVer")
                .appendPara("service", BuildConfig.APPLICATION_ID, false)
                .appendPara("ver", localVer, false)
                .appendPara("evm", "Android_" + Build.VERSION.SDK_INT, false);
        Map<String, Object> req = new HashMap<>();
        req.put("url", url.toString());
        req.put("method", HttpUtil.METHOD_GET);
        req.put("private", false);
        RequestOptions.parse(company, req, IConst.SERVICE_APPSTORE).thenComposeAsync(opts -> {
            return HttpClient.get(opts.url.node, opts.url.url(), opts.headers);
        }, IThreadPool.Pool).whenCompleteAsync((hr, e) -> {
            if(e != null) {
                LOG.error("Fail to get service info from cloud", e);
                return;
            }

            if(hr.code == RetCode.NOT_EXISTS) {
                LOG.info("No update version for {}", url);
                return;
            }

            if(hr.code != RetCode.OK || hr.data == null || hr.data.size() == 0) {
                LOG.error("Fail to get service info from cloud, result:{}", hr.brief());
                return;
            }

            LOG.debug("checkVersion:{}", hr.data);
            int serverVer = ValParser.getAsInt(hr.data, "ver");
            if(localVer < serverVer) {
                this.verFromSrv = StringUtil.intToVer(serverVer);
                this.cdnUrl = ValParser.getAsStr(hr.data, "url");
                this.digest = ValParser.getAsStr(hr.data, "digest");
                this.size = ValParser.getAsInt(hr.data, "size");
                this.features = ValParser.getAsStrList(hr.data, "features");
                context.runOnUiThread(() -> {
                    showUpdateDialog(installPermApply);
                });
            }
        }, IThreadPool.Pool);
    }
}

希望以上内容对你有点帮助,如果有什么问题,欢迎留言评论,我尽量完善它。

此文只在CSDN上编辑修改过,有网站转载了老版本的,里面存在错误,请注意。文章来源地址https://www.toymoban.com/news/detail-690365.html

到了这里,关于Android实现App内自动升级,适配了安卓7、8及以上版本的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Android APP开机启动,安卓APP开发自启动,安卓启动后APP自动启动 Android让程序开机自动运行APP

    第一步设置获取广播后的业务 第二查权限给APP 理论以上两步做完就可以了。APP也能收到广播信息了, 但是APP没有在桌面启动。 经过再研究,发现要在手机再设置自动开启等业务,以下是小米、魅族的系统设置的一些内容,其它平台自己研究。 这里已经显示收到广播信息  

    2024年02月06日
    浏览(52)
  • Android存储权限完美适配(Android11及以上适配)

    一、Bug简述 一个很普通的需求,需要下载图片到本地,我的三个测试机(荣耀Android10,红米 11 和小米Android 13都没有问题)。 然后,主角登场了,测试的三星Android 13 死活拉不起存储权限弹窗。 想了下,三星的系统可能和小米的系统做了些区别。于是就是看了下存储权限的版

    2024年02月06日
    浏览(46)
  • Android app的暗黑模式适配实现

    原文地址: Android app的暗黑模式适配实现 - Stars-One的杂货小窝 很久之前放在草稿箱的一篇简单笔记,是之前蓝奏云批量下载工具Android版本实现暗黑主题的适配记录 本文所说的这里的暗黑主题,应该只支持Android10系统,不过我手头的Flyme系统(Android9)上测试也有效果,其他低版本则没

    2024年02月05日
    浏览(47)
  • burpsuite无法抓取安卓9以上的app数据包问题

    当对app进行渗透测试时发现,android9的系统上burpsuite无法抓取app应用数据包,后经一番搜寻得知扔是证书问题,android9不信任用户安装的证书,那么需要使用adb命令的方式将burpsuite证书导入系统证书中。安卓7的系统好像也是无法抓取的。 首先需要转换证书格式。 需要安装op

    2024年02月05日
    浏览(43)
  • Android打开系统文件管理器,并获取选中文件的路径,适配Android10及以上无法获取路径

    1.进入文件管理器 2.在onActivityResult中获取返回结果 3.FileHelper完整工具类

    2024年02月02日
    浏览(59)
  • 安卓12(高版本9+以上)安装Charles证书到系统证书安装目录

    (1) 安卓手机开启root并安装Magisk (2) 先安装Chalers证书到用户证书安装目录 (3) 下载并安装magisk的adguardcert模块 adguardcert模块下载:https://download.csdn.net/download/weixin_51111267/87929688 (4) 把刚刚安装到用户目录的证书 06c57dd5.0 移动到以下目录 /data/adb/modules/aguardcert/system/etc/security/cacert

    2024年01月17日
    浏览(54)
  • 记录一次uniapp实现APP自动升级

    app的版本管理和升级,是一个不可或缺的功能,而uniapp则是提供了一整套的流程,由于官方文档过于复杂,而且写的云里雾里的,所以个人记录一次我的操作,直到配置成功。 一共分为2个部分,官方提供的两个插件( uni-upgrade-center - Admin 和 uni-upgrade-center - App )配套使用,

    2024年01月23日
    浏览(52)
  • Jupyter-notebook升级内核至Python3.9版本以上

      目录 1、用管理员身份打开Anaconda Prompt 2、Install 3、Activate 4、pywin32_postinstall.py -install  5、打开 6、问题 6.1、Jupyter-notebook默认文件夹位置(Home) 6.2、再次打开3.9版本需要重复操作5、打开 因为调用matplotlib库时出现版本不兼容的问题,我想将Python版本升级到3.9以上,搞了一下午

    2024年02月05日
    浏览(57)
  • 入门级带你实现一个安卓智能家居APP(1)java版本

    ?xml version=\\\"1.0\\\" encoding=\\\"utf-8\\\"? 以上是比较重要的UI控件,大家了解一下,其他的一些属性我就不多说了,你们可以自己尝试修改一下参数,就会发现具体的对应的作用是什么了。 我们的ui已经做好了,现在就去写主活动类的逻辑代码!!! 我主要说说主要逻辑吧。 首先,当然是

    2024年04月27日
    浏览(41)
  • 【教程】安卓设备使用AidLux部署高版本HomeAssistant(2023.2及以上)及安装HACS

    本文发布于:2023年7月1日 备注:Python3.11装起来问题比较多不建议使用         由于AidLux应用商店提供的Python版本最高只支持到3.9,对HomeAssistant的支持只到2023.1版本,而且问题比较多。         本文先安装一个Python3.10。为了能跟系统自带的Python3.7共存,使用源码编译。  

    2024年02月12日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包