改变世界的是这样一群人,他们寻找梦想中的乐园,当他们找不到时,他们亲手创造了它

Android APP 自动更新实现

Android App自动更新基本上是每个App都需具备的功能,参考网上各种资料,自己整理了下,先来看看大致的界面:

一、实现思路:

1.发布Android App时,都会生成output-metadata.json文件和对应的apk文件。(不知道如何打包发布apk,可以网上搜一下)

2.output-metadata.json文件里面就记录了发布的程序版本,通过读取此文件来判断是否需要进行更新。

3.更新过程包括:

①下载Apk文件。

②安装Apk文件。

二、实现步骤:

1.申明权限:由于自动更新需要访问网络,下载更新包,执行安装操作,所以需要申明以下权限:

<!-- 网络权限 -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <!-- 在SDCard中创建与删除文件权限 -->
    <uses-permission
        android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
        tools:ignore="ProtectedPermissions" />
    <!-- 存储权限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <!-- 安装APK权限 -->
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

另外,配置AndroidManifest.xml文件时,还有2个细节需注意下:

(1)由于App更新包放在非https的网站下,需要配置app允许访问非http的网站。

(2)安装App时,Android7.0以上版本需要通过FileProvider方式进行安装,详情可以参考 通过代码安装APK的方法详解:

如果如下配置中<provider android:name="androidx.core.content.FileProvider">报错的话可以替换为:android:name="android.support.v4.content.FileProvider"。

文件:file_paths.xml

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!--安装包文件存储路径-->
    <external-files-path
        name="my_download"
        path="Download" />
    <external-path
        name="."
        path="." />
</paths>

文件:network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

2.权限配置完后,现在就开始制作更新程序了。添加更新进度布局。

文件:progress.xml

<?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:orientation="vertical">
 
    <LinearLayout
        android:id="@+id/titleBar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
 
        <TextView
            android:id="@+id/txtStatus"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="状态"
            android:textSize="10sp"
            android:textStyle="normal" />
 
        <ProgressBar
            android:id="@+id/progress"
            style="?android:attr/progressBarStyleHorizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_toLeftOf="@id/txtStatus" />
    </LinearLayout>
</LinearLayout>

里面就一个显示百分比的文本框,和一个进度条。

3.现在进入更新过程的核心操作阶段了,把检查更新,下载apk,安装apk等操作封装成了一个类。

package com.example.scancode.util;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.support.v4.content.FileProvider;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import com.example.scancode.R;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class AutoUpdater {
    // 下载安装包的网络路径
    private String apkUrl = "http://www.shuilangyizu.top/apk/";
    protected String checkUrl = apkUrl + "output-metadata.json";

    // 保存APK的文件名
    private static final String saveFileName = "shuilangyizu.apk";
    private static File apkFile;

    // 下载线程
    private Thread downLoadThread;
    private int progress;// 当前进度
    // 应用程序Context
    private Context mContext;
    // 是否是最新的应用,默认为false
    private boolean isNew = false;
    private boolean intercept = false;
    // 进度条与通知UI刷新的handler和msg常量
    private ProgressBar mProgress;
    private TextView txtStatus;

    private static final int DOWN_UPDATE = 1;
    private static final int DOWN_OVER = 2;
    private static final int SHOWDOWN = 3;

    public AutoUpdater(Context context) {
        mContext = context;
        apkFile = new File(mContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), saveFileName);
    }

    public void ShowUpdateDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(mContext,R.style.Theme_Scancode);
        builder.setTitle("软件版本更新");
        builder.setMessage("有最新的软件包,请下载并安装!");
        builder.setPositiveButton("立即下载", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                ShowDownloadDialog();
            }
        });
        builder.setNegativeButton("以后再说", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
            }
        });

        builder.create().show();
    }

    private void ShowDownloadDialog() {
        AlertDialog.Builder dialog = new AlertDialog.Builder(mContext,R.style.Theme_Scancode);
        dialog.setTitle("软件版本更新");
        LayoutInflater inflater = LayoutInflater.from(mContext);
        View v = inflater.inflate(R.layout.activity_progress, null);
        mProgress = (ProgressBar) v.findViewById(R.id.progress);
        txtStatus = v.findViewById(R.id.txtStatus);
        dialog.setView(v);
        dialog.setNegativeButton("取消", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                intercept = true;
            }
        });
        dialog.show();
        DownloadApk();
    }

    /**
     * 检查是否更新的内容
     */
    public void CheckUpdate() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                String localVersion = "1";
                try {
                    localVersion = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0).versionName;
                } catch (PackageManager.NameNotFoundException e) {
                    e.printStackTrace();
                }
                String versionName = "1";
                String outputFile = "";
                String config = doGet(checkUrl);

                if (config != null && config.length() > 0) {
                    JSONObject configJson = null;
                    try {
                        configJson = new JSONObject(config);
                        JSONArray elementsJsonArray = configJson.getJSONArray("elements");
                        for (int i = 0; i < elementsJsonArray.length(); i++) {
                            JSONObject elementsJson = (JSONObject) elementsJsonArray.get(i);
                            versionName = elementsJson.getString("versionName");
                            outputFile = elementsJson.getString("outputFile");
                        }

                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                }
                if (Long.parseLong(localVersion) < Long.parseLong(versionName)) {
                    apkUrl = apkUrl + outputFile;
                    mHandler.sendEmptyMessage(SHOWDOWN);
                } else {
                    return;
                }
            }
        }).start();
    }

    /**
     * 从服务器下载APK安装包
     */
    public void DownloadApk() {
        downLoadThread = new Thread(DownApkWork);
        downLoadThread.start();
    }

    private Runnable DownApkWork = new Runnable() {
        @Override
        public void run() {
            URL url;
            try {
                url = new URL(apkUrl);
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
          //必须加上这一句获取的文件才会有大小(原来获取的数据是采用gzip压缩的格式的,所以读不出来数据。下面这句可以解决)
          conn.setRequestProperty("Accept-Encoding", "identity");
          conn.connect();
          int length = conn.getContentLength();
byte[] buf = new byte[1024]; InputStream ins = conn.getInputStream(); FileOutputStream fos = new FileOutputStream(apkFile); int count = 0; while (!intercept) { int numread = ins.read(buf); count += numread; progress = (int) (((float) count / length) * 100); // 下载进度 mHandler.sendEmptyMessage(DOWN_UPDATE); if (numread <= 0) { // 下载完成通知安装 mHandler.sendEmptyMessage(DOWN_OVER); break; } fos.write(buf, 0, numread); } fos.close(); ins.close(); } catch (Exception e) { e.printStackTrace(); } } }; /** * 安装APK内容 */ public void installAPK() { try { if (!apkFile.exists()) { return; } Intent intent = new Intent(Intent.ACTION_VIEW); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//安装完成后打开新版本 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 给目标应用一个临时授权 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {//判断版本大于等于7.0 //如果SDK版本>=24,即:Build.VERSION.SDK_INT >= 24,使用FileProvider兼容安装apk String packageName = mContext.getApplicationContext().getPackageName(); String authority = new StringBuilder(packageName).append(".fileprovider").toString(); Uri apkUri = FileProvider.getUriForFile(mContext, authority, apkFile); intent.setDataAndType(apkUri, "application/vnd.android.package-archive"); } else { intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive"); } mContext.startActivity(intent); //android.os.Process.killProcess(android.os.Process.myPid());安装完之后会提示”完成” “打开”,在android11之后这里不可以执行这一句,会提示解析错误。 } catch (Exception e) { } } private Handler mHandler = new Handler() { public void handleMessage(android.os.Message msg) { switch (msg.what) { case SHOWDOWN: ShowUpdateDialog(); break; case DOWN_UPDATE: txtStatus.setText(progress + "%"); mProgress.setProgress(progress); break; case DOWN_OVER: Toast.makeText(mContext, "下载完毕", Toast.LENGTH_SHORT).show(); installAPK(); break; default: break; } } }; public static String doGet(String httpurl) { HttpURLConnection connection = null; InputStream is = null; BufferedReader br = null; String result = null; try { URL url = new URL(httpurl); connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.setConnectTimeout(15000); connection.setReadTimeout(60000); connection.connect(); if (connection.getResponseCode() == 200) { is = connection.getInputStream(); br = new BufferedReader(new InputStreamReader(is, "UTF-8")); StringBuffer sbf = new StringBuffer(); String temp = null; while ((temp = br.readLine()) != null) { sbf.append(temp); //sbf.append("\r\n"); } result = sbf.toString(); } } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (null != br) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } if (null != is) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } connection.disconnect(); } return result; } }

 

注意:上面的 apkUrl即是发布更新包存放的网络路径。其他操作可以参考代码注释,就不再赘述了。

这里有一个小技巧,可以设置每次打包时,程序按当前时间进行版本号设置。需修改build.gradle文件,像下面这样:

plugins {
    id 'com.android.application'
}
android {
    compileSdkVersion 28
    buildToolsVersion "30.0.3"
    defaultConfig {
        applicationId "com.qingshan.blog"
        minSdkVersion 23
        targetSdkVersion 30
        versionCode 1
        versionName "${releaseTime()}"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            android.applicationVariants.all { variant ->
                variant.outputs.all {
                    outputFileName = "my_${releaseTime()}.apk"
                }
            }
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}
dependencies {
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'com.google.android.material:material:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    implementation 'cn.bingoogolapple:bga-qrcode-zbar:1.3.7'
}
def releaseTime() {
    return new Date().format("yyyyMMddHHmmss", TimeZone.getTimeZone("UTC"))
}

打包后,就可以得到类似的文件结构:

直接将这2个文件复制到发布服务器上进行发布即可。

4.在MainActivity.java中进行检查配置。在onCreate方法中加入代码:

//检查更新
        try {
            //6.0才用动态权限
            if (Build.VERSION.SDK_INT >= 23) {
                String[] permissions = {
                        Manifest.permission.READ_EXTERNAL_STORAGE,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE,
                        Manifest.permission.ACCESS_WIFI_STATE,
                        Manifest.permission.INTERNET};
                List<String> permissionList = new ArrayList<>();
                for (int i = 0; i < permissions.length; i++) {
                    if (ActivityCompat.checkSelfPermission(this, permissions[i]) != PackageManager.PERMISSION_GRANTED) {
                        permissionList.add(permissions[i]);
                    }
                }
                if (permissionList.size() <= 0) {
                    //说明权限都已经通过,可以做你想做的事情去
                    //自动更新
                    AutoUpdater manager = new AutoUpdater(MainActivity.this);
                    manager.CheckUpdate();
                } else {
                    //存在未允许的权限
                    ActivityCompat.requestPermissions(this, permissions, 100);
                }
            }
        } catch (Exception ex) {
            Toast.makeText(MainActivity.this, "自动更新异常:" + ex.getMessage(), Toast.LENGTH_SHORT).show();
        }

处理权限申请

@Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        boolean haspermission = false;
        if (100 == requestCode) {
            for (int i = 0; i < grantResults.length; i++) {
                if (grantResults[i] == -1) {
                    haspermission = true;
                }
            }
            if (haspermission) {
                //跳转到系统设置权限页面,或者直接关闭页面,不让他继续访问
                permissionDialog();
            } else {
                //全部权限通过,可以进行下一步操作
                AutoUpdater manager = new AutoUpdater(MainActivity.this);
                manager.CheckUpdate();
            }
        }
    }
 
    AlertDialog alertDialog;
 
    //打开手动设置应用权限
    private void permissionDialog() {
        if (alertDialog == null) {
            alertDialog = new AlertDialog.Builder(this)
                    .setTitle("提示信息")
                    .setMessage("当前应用缺少必要权限,该功能暂时无法使用。如若需要,请单击【确定】按钮前往设置中心进行权限授权。")
                    .setPositiveButton("设置", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            cancelPermissionDialog();
                            Uri packageURI = Uri.parse("package:" + getPackageName());
                            Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, packageURI);
                            startActivity(intent);
                        }
                    })
                    .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            cancelPermissionDialog();
                        }
                    })
                    .create();
        }
        alertDialog.show();
    }
 
    private void cancelPermissionDialog() {
        alertDialog.cancel();
    }

至此,就完成了apk自动更新功能。

需要注意的几个地方:

1.权限申请一定要对,包括网络权限,存储权限,安装APK权限。

2.代码安装Apk时,需通过FileProvider方式进行安装。

3.程序配置了按时间生成版本号,直接对比版本号来进行判断是否需要更新。

 

原文路径:https://blog.csdn.net/a497785609/article/details/113896609

posted @ 2022-10-19 09:11  水狼一族  阅读(853)  评论(2编辑  收藏  举报
改变世界的是这样一群人,他们寻找梦想中的乐园,当他们找不到时,他们亲手创造了它