安卓应用实现自动升级

news2024/12/23 18:07:05

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

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

     大致步骤如下:

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

       各个安卓版本差异较大,测试日期为(2023.5.29),测试环境为小米8(安卓10)。我不考虑兼容安卓7之前的版本,所以代码中也无相关实现。.

一、AndroidManifest设置

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>

有以下四点需要注意:

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

  4. 在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>

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

安卓5.0之后,申请权限发生过一次大变更,以下实现没有考虑兼容老版本。以下代码是MainActivity中片段。为了方便,requestPermissions返回一个CompletableFuture,来支持流式处理。如果觉得没有必要用CompletableFuture,可以直接在onRequestPermissionsResult中处理,这个只是个人喜好。在MainActivity.onCreate函数中调用它来申请权限。

requestPermissions(new String[]{
        Manifest.permission.WRITE_EXTERNAL_STORAGE,         Manifest.permission.READ_EXTERNAL_STORAGE}).thenComposeAsync(r -> {
        if(r.getInt(HandleResult.CODE) == RetCode.OK) {...}
});
    private static final Map<Integer, MainFuture> MainFutures = new ConcurrentHashMap<>();
    
    private CompletableFuture<Bundle> requestPermissions(String[] permissions) {
        List<String> notGranted = new ArrayList<>();
        for(String p : permissions) { //检查权限,已经有的不必再申请
            int perm = ContextCompat.checkSelfPermission(MainActivity.this, p);
            if (perm != PackageManager.PERMISSION_GRANTED) {
                notGranted.add(p);
            }
        }

        if(notGranted.size() == 0) {
            return CompletableFuture.completedFuture(MainFuture.ok());
        }

        //如果还缺少权限,则需要弹出窗口向用户申请
        int requestCode = MainFuture.getCode();
        ActivityCompat.requestPermissions(MainActivity.this, notGranted.toArray(new String[]{}), requestCode);
        MainFuture mf = new MainFuture();
        MainFutures.put(requestCode, mf);

        return mf.resp;
    }

    /**
     * 申请权限的回调
     * @param requestCode 在调用ActivityCompat.requestPermissions时指定
     * @param permissions 待申请权限名称列表
     * @param grantResults 申请的结果,PackageManager.PERMISSION_GRANTED为成功
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        MainFuture future = MainFutures.get(requestCode);
        for (int i = 0; i < permissions.length; i++) {
            LOG.debug("Grant {},result is {}", permissions[i], grantResults[i]);
        }

        if (future == null) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
            return;
        }

        int errNo = -1;
        for (int i = 0; i < permissions.length; i++) {
            if(grantResults[i] != PackageManager.PERMISSION_GRANTED) {
                errNo = i;
                break;
            }
        }
        Bundle resp;
        if(errNo < 0) {
            resp = MainFuture.ok();
        } else {
            resp = MainFuture.response(RetCode.NO_RIGHT, permissions[errNo] + " not granted");
        }
        future.resp.complete(resp);
        MainFutures.remove(requestCode);
    }

    private static class MainFuture {
        static final AtomicInteger sn = new AtomicInteger(0);

        final CompletableFuture<Bundle> resp = new CompletableFuture<>();

        public static Bundle response(int code, String info) {
            Bundle resp = new Bundle();
            resp.putInt(HandleResult.CODE, code);
            resp.putString(HandleResult.INFO, info);
            return resp;
        }

        public static Bundle ok() {
            return response(RetCode.OK, RetCode.INFO_SUCCESS);
        }

        public static int getCode() {
            return sn.getAndIncrement();
        }
    }

三、申请安装应用

         与申请外部存储读写权限不同,申请安装权限的实现不能用申请其他权限公用,并且registerForActivityResult必须在onCreate中调用,为了避免一安装就申请安装权限,在MainActivity中增加了两个奇怪的成员变量,在真正需要调用requestIntallPermission申请权限时要用到它们。

private int requestInstallCode;
private ActivityResultLauncher<Intent> intentActivityResultLauncher;
    
/*
 * registerForActivityResult必须在onCreate中调用,
 * 否则会报错:LifecycleOwners must call register before they are STARTED.
 * 所以MainActivity中出现了两个奇怪的成员变量,requestInstallCode与intentActivityResultLauncher,
 * intentActivityResultLauncher在requestIntallPermission中会使用。
 * 如果不这样实现,就会刚安装就需要申请安装应用的权限,把用户都吓退了。
 * 在MainActivity.onCreate中需要调用以下方法初始化它们。
 */

requestInstallCode = MainFuture.getCode();
intentActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
(result) -> {
    Bundle resp;
    //此处是跳转的result回调方法
    if (result.getResultCode() != Activity.RESULT_OK) {
        resp = MainFuture.response(RetCode.NO_RIGHT, "No right to install app");
    } else {
        resp = MainFuture.ok();
    }
    MainFuture mf = MainFutures.get(requestInstallCode);
    if(mf != null) {
        mf.resp.complete(resp);
        MainFutures.remove(requestInstallCode);
    }
});


private CompletableFuture<Bundle> requestIntallPermission() {
        boolean haveInstallPermission = getPackageManager().canRequestPackageInstalls();
        if(!haveInstallPermission) {
            MainFuture mf = new MainFuture();
            MainFutures.put(requestInstallCode, mf);
            runOnUiThread(() -> {
                Uri packageURI = Uri.parse("package:" + BuildConfig.APPLICATION_ID);
                Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, packageURI);
                intentActivityResultLauncher.launch(intent);
            });
            return mf.resp;
        }
        return CompletableFuture.completedFuture(MainFuture.ok());
}

        申请外部存储读写权限与申请安装权限需要在MainActivity.onCreate调用,以下是将两个合起来的实现,使用CompletableFuture的好处就是看起来紧凑一点,一个处理过程不会分布在多个Activity的回调函数中:

        /**
         * 调用Environment.getExternalStoragePublicDirectory等函数,
         * 必须具备外部存储读写权限,
         * 除了在manifest中要声明权限,同时在application中设置
         * android:requestLegacyExternalStorage="true"
         * 并且,还需要在代码中动态申请
         */
        String[] permissions = {
                Manifest.permission.WRITE_EXTERNAL_STORAGE,
                Manifest.permission.READ_EXTERNAL_STORAGE
        };
        requestPermissions(permissions).thenComposeAsync(r -> {
            LOG.debug("Request {},code:{},info:{}",
                    permissions, r.getInt(HandleResult.CODE),
                    r.getString(HandleResult.INFO));
            if(r.getInt(HandleResult.CODE) == RetCode.OK) {
                return requestIntallPermission();
            }
            return CompletableFuture.completedFuture(r);
        }, IThreadPool.Pool).whenCompleteAsync((r, e) -> {
            if(e != null) {
                LOG.error("Fail to request permissions {}", permissions, e);
                return;
            }
            if(r.getInt(HandleResult.CODE) == RetCode.OK) {
                Updater updater = new Updater(this);
                updater.checkVersion();
            } else {
                LOG.error("Request Install,code:{},info:{}",
                        r.getInt(HandleResult.CODE),
                        r.getString(HandleResult.INFO));
            }
        });

四、升级安装

        在上面代码片段中,有一个Updater类,这是我的实现,每个应用可以不同,特别是显示的提醒升级对话框、下载用的库都不一样,但是安装操作是相同的,所以只贴这部分代码。

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

        installApk(File apkFile, String digest)

  1. apkFile:已经下载成功的安装文件,我指定的路径是context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)+"/app.apk",似乎autoupdate.xml中的设置在此并未起什么作用;
  2. digest:从我的服务器上查到的文件md5值,安装前会比较校验码是否相同,不同则拒绝安装。
    /**
     * 安装apk
     * @param apkFile apk文件完整路径
     * @param digest 校验码
     */
    private void installAPK(File apkFile, String digest) {
        try {
            if (!apkFile.exists()) {
                LOG.error("Update apk file `{}` not exists", apkFile);
                return;
            }

            String localDigest = FileUtil.digest(apkFile);
            if(!localDigest.equals(digest)) {
                LOG.error("Invalid apk file `{}` digest({}!={})", apkFile, localDigest, 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);
        }
    }

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/584335.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Unity WebGl和前端(Angular)相互调用(含跨域问题)

在Unity官方文档中就已经介绍了Unity和JS相互调用的问题&#xff0c;但是我们实际的应用中往往是使用iframe来展示WebGL。这样不但是webgl和js相互调用的问题&#xff0c;还包含了iframe跨域的问题。 我们的项目中前端使用的是angular框架&#xff0c;就以angular为基础来说一…

关于谷歌云计算进行Python yolov5的使用操作(简略版)

关于谷歌云计算的使用操作&#xff08;简略版&#xff09; 谷歌云盘&#xff1a;https://drive.google.com/ 翻墙订阅&#xff1a;链接 谷歌云盘创建操作&#xff1a; 1 2 3.添加Colaboratory关联 设置免费的GPU 它是很容易更换默认的硬件&#xff08;None,GPU,TPU&#xff…

群智能算法-粒子群1

一.基本理念 粒子群算法(PSO),最早由两位外国科学家在1995年提出&#xff0c;该算法源自对鸟类捕食问题的研究。 我们将鸟类补食性原则分为一下三个基数&#xff0c;每组基数对应一个粒子&#xff0c;而每组粒子我们将其称之为种群。所以我们将其在细分具体&#xff0c;基数一&…

C++核心编程—类和对象,类的三大特性——封装、继承、多态

纵有疾风起&#xff0c;人生不言弃。本文篇幅较长&#xff0c;如有错误请不吝赐教&#xff0c;感谢支持。 &#x1f4ac;文章目录 一.类和对象的概念①什么是对象&#xff1f;②抽象和类1.类的基本概念2.类的声明与定义&#xff1a;3.对象的创建与使用 二.类的封装①为什么有封…

阿里云国际站代理商:阿里云支持哪些大数据方案和应用?如何使用和操作?

阿里云国际站代理商&#xff1a;阿里云支持哪些大数据方案和应用&#xff1f;如何使用和操作&#xff1f; [本文由阿里云代理商聚搜云撰写] 大数据时代已经来临&#xff0c;如何处理并分析这些海量的数据成了企业迫切需要解决的问题。作为一家国内领先的云计算提供商&#xff0…

传地址给组件并让该组件用到地址在背景图片中的方法

问题 这是我在开发中遇到的问题。在网站的分页面中&#xff0c;背景图片的格式几乎一模一样。只是上面的文字和图片不一样而已。 所以我希望写一个组件&#xff0c;然后页面只需要传入背景图片地址和标题就可以显示出相关内容。 于是我动手写了&#xff0c;大致思路是一个盒…

Win11的两个实用技巧系列之读取硬盘很卡的解决办法、添加防火墙信任项方法

win11读取机械硬盘速度慢? win11读取硬盘很卡的解决办法 win11读取机械硬盘速度慢&#xff1f;win11系统硬盘读取速度太慢了&#xff0c;该怎么解决呢&#xff1f;下面我们就来看看win11读取硬盘很卡的解决办法 不少win11用户都遇到了读取硬盘很卡的情况&#xff0c;导致等待…

2023年铜川宜宾半程马拉松赛-人生首次半马比赛

1、赛事背景 2023年5月21日&#xff0c;我参加了2023年铜川宜宾半程马拉松赛&#xff0c;也是人生首次半马比赛。。很久之前的天气预报就是说要下雨&#xff0c;有的时候更是预报中大雨&#xff0c;所以很担心半马会不会延期举办&#xff0c;还好如期举行了。 2023宜君“药谷飘…

盐城北大青鸟东台基地IT精英挑战赛作品展示

北大青鸟IT精英挑战赛作品展示 ——《波比》 这是我们21级老生们的作品&#xff0c;主要的创作灵感来自于&#xff0c;可爱的小青蛙和王子小时候结合而成的IP形象&#xff0c;同时也是漫展的追随者。 设计团队21级计算机2班&#xff1a;王丽娜 陈美玲 陆梅莹

深度学习实战——模型推理优化(模型压缩与加速)

忆如完整项目/代码详见github&#xff1a;https://github.com/yiru1225&#xff08;转载标明出处 勿白嫖 star for projects thanks&#xff09; 目录 系列文章目录 一、实验思路综述 1.实验工具及内容 2.实验数据 3.实验目标 4.实验步骤 二、模型压缩与加速综述 1.模…

C++中的高阶函数:以std::function优雅地实现回调

C中的高阶函数&#xff1a;以std::function优雅地实现回调 1. 简介1.1 C高阶函数的概念1.2 C的std::function的功能及其重要性 2. std::function的使用2.1 std::function的定义和基本使用2.1.1 std::function的定义2.1.2 std::function的基本使用 2.2 std::function接受普通函数…

安吉尔航天净水新品发布,净水行业已进入新赛点?

作为具有较强线下依赖性的家电细分市场&#xff0c;净水器行业受到外部因素的扰动较大&#xff0c;2020年&#xff0c;经济下行趋势明显&#xff0c;这一年也成为国内净水器市场的拐点&#xff0c;不少业内人士认为多年的行业扩张期已在此结束。 但进入2023年&#xff0c;随着…

【收藏】麻省理工:如何选择和设计论文的Figure?

论文中的图表以独有的方式组织信息&#xff0c;更好地传递作者思想。那么&#xff0c;如何选择和设计合适的Figure&#xff1f; MIT Communication Lab&#xff08;麻省理工学院通信实验室&#xff09;为作者提供了有效的建议&#xff0c;我们来学习一下 ~ 01 数据图 在制作数…

使用 LSSVM 的 Matlab 演示求解反常微分方程问题(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

Qt Quick系列(3)—组件component

&#x1f680;作者&#xff1a;CAccept &#x1f382;专栏&#xff1a;Qt Quick 文章目录 概念相关知识点代码示例总结 概念 在Qt Quick中&#xff0c;组件&#xff08;Component&#xff09;是一种可重用的元素&#xff0c;可以包含其他子组件或属性。它们可以用来创建自定…

PIC adc模块的配置

PIC adc模块的配置有如下要点&#xff1a; 1. adc模块本身&#xff1a; 注意&#xff0c;Auto-conversion Trigger和ADC的clock是两个概念。 auto-conversion Trigger的频率不得超过ADC采样一次的总时长。而真正的采样率是auto-conversion Trigger的頻率。 采样的过程中&…

2023音视频开发程序员未来10年路线选择

2023音视频开发程序员未来10年路线选择&#xff1a; 音视频领域&#xff0c;其实你可以分三个部分来看&#xff0c; 第一是音视频本身&#xff0c;第二是网络通讯&#xff0c;第三是图像处理。 音视频本身涉及到音视频视频编解码啊&#xff0c;各种视频容器啊等等协议规范。 网…

【Java SE】| Java 序列化详解

目录 &#x1f981; 什么是序列化和反序列化?&#x1f981; 序列化和反序列化常见应用场景&#x1f981; 序列化协议对应于 TCP/IP 4 层模型的哪一层&#xff1f;&#x1f981; 常见序列化协议有哪些&#xff1f;1. Java自带的序列化方式2. Kryo3.Hessian &#x1f981; 什么是…

Dockerfile(6) - EXPOSE 指令详解

EXPOSE 通知 Docker 容器在运行时监听指定的网络端口 EXPOSE 端口号 EXPOSE 端口号/协议 默认协议是 TCP 同时在 TCP、UDP 上暴露端口 EXPOSE 80/tcp EXPOSE 80/udp EXPOSE 原理 个人理解&#xff1a;EXPOSE 暴露的端口更像是指明了该容器提供的服务需要用到的端口EXPOSE …

独角数卡 搭建-邮件配置-Epusdt配置-收U详细配置

配置独角数卡 https://github.com/assimon/dujiaoka/wiki/2.x_bt_install ⚠️正式上线后一定要将.env配置里面的APP_DEBUG设置为false⚠️ ⚠️正式上线后一定要将.env配置里面的APP_DEBUG设置为false⚠️ ⚠️正式上线后一定要将.env配置里面的APP_DEBUG设置为false⚠️ 安…