? ? ? ? 應(yīng)用發(fā)布后,要實現(xiàn)灰度升級控制,如果只依賴各家應(yīng)用市場是不夠的,還需要自己在應(yīng)用中控制升級邏輯。并且每家應(yīng)用市場上新審核也是一件很麻煩的事情,尤其像至簡網(wǎng)格這樣的應(yīng)用,甚至沒在應(yīng)用市場上架,更不可能依賴它們了。所以必須要在應(yīng)用中實現(xiàn)自動升級功能。
????????網(wǎng)上有很多介紹,他們摸索的結(jié)果對我有很大幫助。可能是因為版本關(guān)系,或者關(guān)注點不同,照著做,會有很多過時的或錯誤的地方,所以我將摸索過程記錄在此,防止忘記。
????????下面幾個圖是在華為榮耀V9(安卓7.0、SDK 24)中的界面:
圖1、提醒有可升級的版本
圖2、下載版本
圖3、安卓7.0的安全檢測界面?
?????大致步驟如下:
- AndroidManifest及res設(shè)置;
- 申請外部存儲讀寫權(quán)限;
- 申請安裝應(yīng)用;
- 向服務(wù)端查詢是否有可升級版本,下載版本,執(zhí)行安裝;
? ? ? ?安卓各個版本差異較大,我的測試日期為(2023.5.29),測試環(huán)境為小米8(安卓10、SDK29)、華為榮耀v9(安卓7.0、SDK 24)兩種。因為不考慮兼容安卓7之前的版本,所以代碼中也無相關(guān)實現(xiàn)。
一、AndroidManifest及res設(shè)置
1、AndroidManifest設(shè)置
增加以下權(quán)限:
<!-- 網(wǎng)絡(luò)權(quán)限,不用在程序中動態(tài)申請 -->
<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"/>
<!-- 外部存儲讀寫,需要在程序中動態(tài)申請,用于存儲運行日志,以及下載的升級版本-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 安裝APK權(quán)限,需要在程序中動態(tài)申請,并且不同于外部存儲讀寫權(quán)限申請 -->
<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名稱在安裝時傳遞給系統(tǒng)安裝程序 -->
<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",這個名稱可以自己定,但是在執(zhí)行安裝時必須保持一致,后面會再次提到;
? -
provider中meta-data->android:resource="@xml/autoupdate"名稱可以自己定,但是需確保在res/xml/下有同名的xml文件,Android7.0及以上版本需要通過FileProvider方式進行安裝,文件內(nèi)容見下一節(jié);
2、res中的準備
- 在res中新建一個xml目錄,創(chuàng)建autoupdate.xml,內(nèi)容如下,注意其中的注釋;
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 如果不設(shè)置root,將會發(fā)生“Failed to find configured root that contains...”錯誤 -->
<root-path name="root_path" path="."/>
<!-- name與path,好像并無太多限制,請了解的同學(xué)指正以下 -->
<external-path name="autoupdate" path="download/" />
</paths>
- 下載安裝界面定義
????????在res/layout中增加download_dlg.xml,用以顯示下載進度及安裝中碰到的問題,怎樣顯示請看后面的Updater類實現(xiàn)。
<?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>
??????
二、申請外部存儲讀寫權(quán)限與安裝權(quán)限
????????安卓5.0之后,申請權(quán)限的操作變化很大,使用起來比老版本好一些,同一個邏輯不會被打散到多個地方實現(xiàn)。以下實現(xiàn)在MainActivity.onCreate中調(diào)用,沒有考慮兼容老版本。里面有個updater.checkVersion調(diào)用,后面會講到,用來查詢服務(wù)端是否有新版本,可以根據(jù)自己的需要做不同的實現(xiàn)。因為只有在具備外部存儲讀寫權(quán)限后才可以執(zhí)行升級操作,所以申請成功后,才會檢查是否有可升級版本。如果沒有新版本,是不會出現(xiàn)申請安裝應(yīng)用權(quán)限的界面,否則系統(tǒng)的提示會嚇退一部分用戶。
?????????申請安裝權(quán)限的實現(xiàn)與申請外部存儲讀寫權(quán)限不同,在安卓8.0(SDK26)后有一次大變動。在后面的Updater類中,如果是8.0之前的版本則直接安裝,否則要申請權(quán)限。
//申請必要的權(quán)限
Updater updater = new Updater(this);
/*
* 申請安裝應(yīng)用的權(quán)限。
* registerForActivityResult必須在onCreate中調(diào)用,
* 否則會報錯:LifecycleOwners must call register before they are STARTED.
*/
ActivityResultLauncher<Intent> installApkLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
(result) -> {//安裝申請確認完畢后的回調(diào)
if (result.getResultCode() == Activity.RESULT_OK) {
updater.showDownloadDialog();
}
});
/*
* 申請外部存儲卡讀寫權(quán)限。
* 調(diào)用Environment.getExternalStoragePublicDirectory等函數(shù),必須具備外部存儲讀寫權(quán)限,
* 除了在manifest中要聲明權(quán)限,同時在application中設(shè)置android:requestLegacyExternalStorage="true"
* 并且,還需要在代碼中動態(tài)申請。
* 申請成功后才能確定應(yīng)用升級可以執(zhí)行下去,所以才會查詢新版本。
*/
registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(),
result -> {//權(quán)限申請執(zhí)行完畢后的回調(diào)
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) { //有了外部存儲讀寫權(quán)限之后再判斷是否有升級版本
updater.checkVersion(installApkLauncher);
}
}
).launch(new String[] {
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
});
? ? ??
三、升級安裝
? ? ? ? 檢查版本、下載、安裝,都在Updater類中實現(xiàn),在申請外部存儲讀寫權(quán)限、申請安裝權(quán)限時,會調(diào)用到Updater中的函數(shù)。
? ? ? ? 代碼中出現(xiàn)的cn.net.zhijian包下的類都是我的公共類,看的時候可以忽略,根據(jù)函數(shù)名稱應(yīng)該能大致猜出它的功能。
????????注意其中的String authority = BuildConfig.APPLICATION_ID + ".fileprovider";前面提到過,必須與provider定義中保持一致。否則會提示Couldn't find meta-data for provider with authority...錯誤。
? ? ?installApk(File apkFile, String digest, AbsHttpCallback.IDownloadProgress progress)
- apkFile:已下載的安裝文件,我指定的路徑是context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)+"/app.apk",似乎autoupdate.xml中的設(shè)置在此并未起什么作用;
- digest:從我的服務(wù)器上查到的文件md5值,安裝前比較校驗碼,不同則拒絕安裝;
- progress:用以提示下載進度、安裝錯誤信息等;
??showUpdateDialog(ActivityResultLauncher<Intent> installPermApply)
??????installPermApply是在MainActivity.onCreate中初始化安裝權(quán)限申請加載器時傳遞進來的。安卓8.0及以上版本才會調(diào)用它,其他情況則直接顯示下載安裝界面。
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; //服務(wù)端返回的應(yīng)用版本號
private String cdnUrl; //服務(wù)端返回的CDN頭部地址,后面加上/app_id/version/app.apk
private String digest; //服務(wù)端返回的應(yīng)用apk校驗碼
private int size; //服務(wù)端返回的應(yīng)用apk大小
private List<String> features; //服務(wù)端返回的新版本的特性列表
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) { //如果已經(jīng)有權(quán)限,不必再申請
Uri packageURI = Uri.parse("package:" + BuildConfig.APPLICATION_ID);
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, packageURI);
installPermApply.launch(intent); //權(quán)限申請通過后執(zhí)行showDownloadDialog
return;
}
}
showDownloadDialog();// 版本<26(Android 8)或已申請了權(quán)限,則直接顯示下載安裝
});
builder.setNegativeButton(R.string.do_it_later, (DialogInterface dialog, int which) -> {
dialog.dismiss();
});
builder.create().show();
}
/**
* 顯示下載對話框,在其中顯示下載、安裝的進度,
* 如果發(fā)生錯誤,也會顯示錯誤信息
*/
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 打印消息的回調(diào)
*/
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); // 給目標應(yīng)用一個臨時授權(quán)
//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);
}
}
希望以上內(nèi)容對你有點幫助,如果有什么問題,歡迎留言評論,我盡量完善它。文章來源:http://www.zghlxwxcb.cn/news/detail-690365.html
此文只在CSDN上編輯修改過,有網(wǎng)站轉(zhuǎn)載了老版本的,里面存在錯誤,請注意。文章來源地址http://www.zghlxwxcb.cn/news/detail-690365.html
到了這里,關(guān)于Android實現(xiàn)App內(nèi)自動升級,適配了安卓7、8及以上版本的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!