Android realizes automatic upgrade in App, adapted to Android 7, 8 and above

        After the application is released, in order to realize grayscale upgrade control, it is not enough to rely only on various application markets, and it is necessary to control the upgrade logic in the application itself. In addition, it is very troublesome to review new applications in each application market, especially for applications such as Simple Grid , which are not even on the shelves of the application market, and it is impossible to rely on them. Therefore, the automatic upgrade function must be implemented in the application.

        There are many introductions on the Internet, and the results of their exploration are of great help to me. It may be because of version relationship or different focus. If you follow it, there will be many outdated or wrong places, so I will record the groping process here to prevent forgetting.

        The following pictures are the interfaces in Huawei Honor V9 (Android 7.0, SDK 24):

Figure 1. Reminder that there is an upgradeable version

Figure 2. Download version

Figure 3. The security detection interface of Android 7.0 

     The general steps are as follows:

  1. AndroidManifest and res settings;
  2. Apply for external storage read and write permissions;
  3. Apply for the installation of the application;
  4. Query the server whether there is an upgradeable version, download the version, and execute the installation;

       The different versions of Android are quite different. My test date is (2023.5.29), and the test environment is Xiaomi 8 (Android 10, SDK29), Huawei Honor v9 (Android 7.0, SDK 24). Because compatibility with versions prior to Android 7 is not considered, there is no related implementation in the code.

1. AndroidManifest and res settings

1. AndroidManifest settings

Add the following permissions:

<!--  网络权限,不用在程序中动态申请 -->
    <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>

There are the following points to note:

  • Application needs to add attribute android:requestLegacyExternalStorage="true";
    
  • The provider attribute android:authorities="${applicationId} .fileprovider ", this name can be set by yourself, but it must be consistent when performing the installation, which will be mentioned again later;
     
  • The name of meta-data->android:resource="@xml/ autoupdate " in the provider can be determined by yourself, but you need to ensure that there is an xml file with the same name under res/xml/. Android7.0 and above versions need to be installed through FileProvider. The content of the file is shown in the next section;
    

    2. Preparation in res

  • Create a new xml directory in res, create autoupdate.xml, the content is as follows, pay attention to the notes;
<?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>
  • Download and install interface definition

        Add download_dlg.xml in res/layout to display the download progress and problems encountered during installation. For how to display it, please refer to the implementation of the Updater class later.

<?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>

      

2. Apply for external storage read and write permissions and installation permissions

        After Android 5.0, the operation of applying for permissions has changed a lot, and it is better to use than the old version, and the same logic will not be scattered to multiple places for implementation. The following implementations are called in MainActivity.onCreate, and compatibility with older versions is not considered. There is an updater.checkVersion call, which will be mentioned later, to check whether there is a new version on the server side, and can be implemented differently according to your needs. Because the upgrade operation can only be performed after having read and write permissions for external storage, it will be checked whether there is an upgradeable version after the application is successful. If there is no new version, the interface for applying for permission to install the app will not appear, otherwise the system prompt will scare off some users.

         The implementation of applying for installation permissions is different from applying for external storage read and write permissions. There is a major change after Android 8.0 (SDK26). In the following Updater class, if it is a version before 8.0, install it directly, otherwise, apply for permission.

        //申请必要的权限
        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
        });

      

3. Upgrade installation

        Checking the version, downloading, and installing are all implemented in the Updater class. When applying for external storage read and write permissions, and applying for installation permissions, the functions in the Updater will be called.

        The classes under the cn.net.zhijian package that appear in the code are all my public classes, which can be ignored when you look at it. You should be able to roughly guess its function according to the function name.

        Note that the String authority = BuildConfig.APPLICATION_ID + ". fileprovider "; as mentioned earlier, must be consistent with the provider definition. Otherwise, the Couldn't find meta-data for provider with authority... error will be prompted.

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

  1. apkFile: The downloaded installation file, the path I specified is context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)+"/app.apk", it seems that the settings in autoupdate.xml have no effect here;
  2. Digest: The md5 value of the file found on my server, compare the check code before installation, and refuse to install if it is different;
  3. progress: used to prompt download progress, installation error information, etc.;
  showUpdateDialog(ActivityResultLauncher<Intent> installPermApply)

      installPermApply is passed in when initializing the installation permission application loader in MainActivity.onCreate. It will only be called on Android 8.0 and above, and in other cases, the download and installation interface will be displayed directly.

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);
    }
}

I hope the above content is helpful to you. If you have any questions, please leave a message and comment. I will try my best to improve it.

This article has only been edited and modified on CSDN. Some websites reprinted the old version, and there are errors in it. Please pay attention.

Guess you like

Origin blog.csdn.net/flyinmind/article/details/130925867
Recommended