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