Android實(shí)現(xiàn)Android?APP自動(dòng)更新功能
一、項(xiàng)目介紹
在移動(dòng)應(yīng)用的全生命周期中,版本迭代和用戶更新體驗(yàn)至關(guān)重要。傳統(tǒng)的做法是依賴 Google Play 商店強(qiáng)制推送更新,但在某些場(chǎng)景下,我們需要:
更即時(shí)地控制更新流程(如灰度、強(qiáng)制升級(jí)、提醒升級(jí)等);
支持市場(chǎng)外分發(fā),比如企業(yè)內(nèi)部應(yīng)用分發(fā)、第三方應(yīng)用商店;
自定義更新 UI,與應(yīng)用風(fēng)格保持一致。
本項(xiàng)目示例將展示兩種主流方案:
Google Play In?App Updates(官方方案,適用于上架 Play 商店的應(yīng)用)
自建服務(wù) + APK 下載 & 安裝(適用于非 Play 分發(fā)場(chǎng)景)
通過(guò)本教程,你將學(xué)會(huì)如何在應(yīng)用內(nèi)檢測(cè)新版本、彈出升級(jí)對(duì)話框、后臺(tái)下載 APK、以及無(wú)縫觸發(fā)安裝流程,極大提升用戶體驗(yàn)。
二、相關(guān)知識(shí)
Google Play Core Library
com.google.android.play:core:1.x.x包含了 In?App Updates API,讓應(yīng)用可在運(yùn)行時(shí)檢查并觸發(fā)“靈活更新”或“立即更新”流程,無(wú)需用戶去 Play 商店界面。
FileProvider & 安裝意圖
對(duì)于自建更新方案,需要在
AndroidManifest.xml配置FileProvider,并通過(guò)Intent.ACTION_VIEW攜帶 APK 的content://URI,調(diào)用系統(tǒng)安裝界面。
WorkManager / DownloadManager
長(zhǎng)任務(wù)(如后臺(tái)下載 APK)應(yīng)使用
WorkManager或系統(tǒng)DownloadManager,保證下載可在后臺(tái)穩(wěn)定運(yùn)行,且重啟后可續(xù)傳。
運(yùn)行時(shí)權(quán)限 & 兼容性
Android 8.0+(API 26+)安裝需獲取 “允許安裝未知應(yīng)用” 權(quán)限 (
REQUEST_INSTALL_PACKAGES)。Android 7.0+(API 24+)文件 URI 必須走
FileProvider,否則會(huì)拋FileUriExposedException。
三、項(xiàng)目實(shí)現(xiàn)思路
版本檢測(cè)
Play 方案:調(diào)用 Play Core 的
AppUpdateManager.getAppUpdateInfo()檢查更新?tīng)顟B(tài)。自建方案:向自有服務(wù)器發(fā)起網(wǎng)絡(luò)請(qǐng)求(如 GET
/latest_version.json),獲取最新版本號(hào)、APK 下載地址、更新說(shuō)明等。
彈窗交互
根據(jù)策略選擇“立即更新”(強(qiáng)制)或“靈活更新”(允許后臺(tái)運(yùn)行時(shí)再重啟安裝),并展示更新日志。
下載 APK
Play 方案:由 Play Core 自動(dòng)下載。
自建方案:用
DownloadManager啟動(dòng)下載,并監(jiān)聽(tīng)廣播獲取下載完成通知。
觸發(fā)安裝
下載完成后,構(gòu)造
Intent.ACTION_VIEW,指定 MIME 類型application/vnd.android.package-archive,使用FileProvider共享 APK URI,啟動(dòng)安裝流程。
四、完整代碼(All?in?One,含詳細(xì)注釋)
// =======================================
// 文件: AutoUpdateManager.java + MainActivity
// (本示例將 Manager 與 Activity 合寫于一處,注釋區(qū)分)
// =======================================
package com.example.autoupdate;
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.DownloadManager;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.FileProvider;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
// —— Play Core 庫(kù)依賴(立即更新/靈活更新)
// implementation "com.google.android.play:core:1.10.3"
import com.google.android.play.core.appupdate.AppUpdateInfo;
import com.google.android.play.core.appupdate.AppUpdateManagerFactory;
import com.google.android.play.core.install.model.AppUpdateType;
import com.google.android.play.core.install.model.UpdateAvailability;
import com.google.android.play.core.tasks.Task;
import org.json.JSONObject;
import java.io.File;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Scanner;
public class MainActivity extends AppCompatActivity {
// ---------- 常量區(qū) ----------
private static final int REQUEST_CODE_UPDATE = 100; // Play 更新請(qǐng)求碼
private static final int REQUEST_INSTALL_PERMISSION = 101; // 動(dòng)態(tài)安裝權(quán)限
private static final String TAG = "AutoUpdate";
private long downloadId; // DownloadManager 返回 ID
private DownloadManager downloadManager;
// ---------- onCreate ----------
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); // 布局見(jiàn)下文
// 按鈕觸發(fā)兩種更新
Button btnPlayUpdate = findViewById(R.id.btnPlayUpdate);
Button btnCustomUpdate = findViewById(R.id.btnCustomUpdate);
btnPlayUpdate.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
checkPlayUpdate(); // Play 商店內(nèi)更新
}
});
btnCustomUpdate.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
checkCustomUpdate(); // 自建服務(wù)器更新
}
});
// 初始化 DownloadManager
downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
// 注冊(cè)下載完成廣播
registerReceiver(onDownloadComplete, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
// ---------- 1. Play In?App Updates 檢查 ----------
private void checkPlayUpdate() {
// 創(chuàng)建 AppUpdateManager
com.google.android.play.core.appupdate.AppUpdateManager appUpdateManager =
AppUpdateManagerFactory.create(this);
// 異步獲取更新信息
Task<AppUpdateInfo> appUpdateInfoTask = appUpdateManager.getAppUpdateInfo();
appUpdateInfoTask.addOnSuccessListener(info -> {
// 判斷是否有更新且支持立即更新
if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
try {
// 發(fā)起靈活更新請(qǐng)求
appUpdateManager.startUpdateFlowForResult(
info,
AppUpdateType.FLEXIBLE,
this,
REQUEST_CODE_UPDATE);
} catch (Exception e) {
Log.e(TAG, "Play 更新啟動(dòng)失敗", e);
}
} else {
Toast.makeText(this, "無(wú)可用更新或不支持此更新類型", Toast.LENGTH_SHORT).show();
}
});
}
// ---------- 2. 自建服務(wù)器版本檢測(cè) ----------
private void checkCustomUpdate() {
new Thread(() -> {
try {
// 1) 請(qǐng)求服務(wù)器 JSON
URL url = new URL("https://your.server.com/latest_version.json");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setRequestMethod("GET");
InputStream in = conn.getInputStream();
Scanner sc = new Scanner(in).useDelimiter("\\A");
String json = sc.hasNext() ? sc.next() : "";
JSONObject obj = new JSONObject(json);
final int serverVersionCode = obj.getInt("versionCode");
final String apkUrl = obj.getString("apkUrl");
final String changeLog = obj.getString("changeLog");
// 2) 獲取本地版本號(hào)
int localVersionCode = getPackageManager()
.getPackageInfo(getPackageName(), 0).versionCode;
if (serverVersionCode > localVersionCode) {
// 有新版,回到主線程彈窗提示
runOnUiThread(() ->
showUpdateDialog(apkUrl, changeLog)
);
} else {
runOnUiThread(() ->
Toast.makeText(this, "已是最新版本", Toast.LENGTH_SHORT).show()
);
}
} catch (Exception e) {
Log.e(TAG, "檢查更新失敗", e);
}
}).start();
}
// ---------- 3. 彈出更新對(duì)話框 ----------
private void showUpdateDialog(String apkUrl, String changeLog) {
new AlertDialog.Builder(this)
.setTitle("發(fā)現(xiàn)新版本")
.setMessage(changeLog)
.setCancelable(false)
.setPositiveButton("立即更新", (dialog, which) -> {
startDownload(apkUrl);
})
.setNegativeButton("稍后再說(shuō)", null)
.show();
}
// ---------- 4. 啟動(dòng)系統(tǒng) DownloadManager 下載 APK ----------
private void startDownload(String apkUrl) {
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apkUrl));
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI
| DownloadManager.Request.NETWORK_MOBILE);
request.setTitle("正在下載更新包");
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "update.apk");
// 開(kāi)始下載
downloadId = downloadManager.enqueue(request);
}
// ---------- 5. 監(jiān)聽(tīng)下載完成,觸發(fā)安裝 ----------
private BroadcastReceiver onDownloadComplete = new BroadcastReceiver() {
@Override public void onReceive(Context context, Intent intent) {
long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
if (id != downloadId) return;
// 下載完成,安裝 APK
File apkFile = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS), "update.apk");
// Android 8.0+ 需要請(qǐng)求安裝未知應(yīng)用權(quán)限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
boolean canInstall = getPackageManager().canRequestPackageInstalls();
if (!canInstall) {
// 請(qǐng)求“安裝未知應(yīng)用”權(quán)限
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.REQUEST_INSTALL_PACKAGES},
REQUEST_INSTALL_PERMISSION);
return;
}
}
installApk(apkFile);
}
};
// ---------- 6. 處理未知來(lái)源權(quán)限申請(qǐng)結(jié)果 ----------
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
if (requestCode == REQUEST_INSTALL_PERMISSION) {
if (grantResults.length>0 && grantResults[0]==PackageManager.PERMISSION_GRANTED) {
// 再次觸發(fā)安裝(假設(shè) APK 仍在下載目錄)
File apkFile = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS), "update.apk");
installApk(apkFile);
} else {
Toast.makeText(this, "安裝權(quán)限被拒絕,無(wú)法自動(dòng)更新", Toast.LENGTH_LONG).show();
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
// ---------- 7. 安裝 APK 輔助方法 ----------
private void installApk(File apkFile) {
Uri apkUri = FileProvider.getUriForFile(this,
getPackageName() + ".fileprovider", apkFile);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(this, "無(wú)法啟動(dòng)安裝程序", Toast.LENGTH_LONG).show();
}
}
// ---------- 8. Activity 銷毀時(shí)注銷 Receiver ----------
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(onDownloadComplete);
}
}<!-- ======================================
文件: AndroidManifest.xml
注意:需要配置 FileProvider 與權(quán)限
====================================== -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.autoupdate">
<!-- 安裝未知來(lái)源權(quán)限(Android 8.0+ 需動(dòng)態(tài)申請(qǐng)) -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<!-- 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/file_paths" />
</provider>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest><!-- ======================================
文件: res/xml/file_paths.xml
FileProvider 路徑配置
====================================== -->
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="download" path="Download/"/>
</paths><!-- ======================================
文件: res/layout/activity_main.xml
簡(jiǎn)單示例界面
====================================== -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:gravity="center"
android:layout_width="match_parent" android:layout_height="match_parent"
android:padding="24dp">
<Button
android:id="@+id/btnPlayUpdate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Play In?App 更新"/>
<View android:layout_height="16dp" android:layout_width="match_parent"/>
<Button
android:id="@+id/btnCustomUpdate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="自建服務(wù)更新"/>
</LinearLayout>五、方法解讀
checkPlayUpdate()
檢查 Google Play 上的更新可用性,并以“靈活更新”方式啟動(dòng)下載和安裝流程。checkCustomUpdate()
通過(guò)HttpURLConnection請(qǐng)求服務(wù)器 JSON,解析最新versionCode與apkUrl,對(duì)比本地版本,決定是否彈窗。showUpdateDialog(...)
基于服務(wù)器返回的changeLog構(gòu)建AlertDialog,提供“立即更新”與“稍后再說(shuō)”兩種交互。startDownload(String apkUrl)
使用系統(tǒng)DownloadManager發(fā)起后臺(tái)下載,保存至公開(kāi)目錄,支持?jǐn)帱c(diǎn)續(xù)傳和系統(tǒng)下載通知。BroadcastReceiver onDownloadComplete
監(jiān)聽(tīng)DownloadManager.ACTION_DOWNLOAD_COMPLETE廣播,確認(rèn)是本次下載后觸發(fā)安裝流程。onRequestPermissionsResult(...)
處理 Android 8.0+ “安裝未知來(lái)源”權(quán)限授權(quán)結(jié)果,授權(quán)后繼續(xù)調(diào)用installApk()。installApk(File apkFile)
通過(guò)FileProvider獲取 APK 的 content URI,并以Intent.ACTION_VIEW調(diào)用系統(tǒng)安裝器。
六、項(xiàng)目總結(jié)
優(yōu)勢(shì)
Play Core In?App 更新:官方支持,體驗(yàn)與 Play 商店一致,無(wú)需手工管理下載邏輯。
自建方案:靈活可控,支持任意分發(fā)渠道,自定義 UI 與灰度策略。
注意與優(yōu)化
權(quán)限與兼容
Android 7.0+ 必須使用
FileProvider。Android 8.0+ 需動(dòng)態(tài)申請(qǐng)
REQUEST_INSTALL_PACKAGES。
下載失敗重試
可結(jié)合
WorkManager增加重試與網(wǎng)絡(luò)斷線重連邏輯。
安全性
建議對(duì) APK 做簽名校驗(yàn)(計(jì)算 SHA256 與服務(wù)器比對(duì)),防止被篡改。
UI 體驗(yàn)
對(duì)“立即更新”與“后臺(tái)更新”作更多狀態(tài)提示。
可顯示下載進(jìn)度條、進(jìn)度通知等。
灰度/強(qiáng)制升級(jí)
可在服務(wù)器 JSON 中添加策略字段,如
forceUpdate,在對(duì)話框中禁止“稍后再說(shuō)”。
到此這篇關(guān)于Android實(shí)現(xiàn)Android APP自動(dòng)更新功能的文章就介紹到這了,更多相關(guān)Android APP自動(dòng)更新內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android中將View的內(nèi)容保存為圖像的簡(jiǎn)單實(shí)例
這篇文章主要介紹了Android中將View的內(nèi)容保存為圖像的簡(jiǎn)單實(shí)例,有需要的朋友可以參考一下2014-01-01
Android開(kāi)發(fā)入門環(huán)境快速搭建實(shí)戰(zhàn)教程
最近想重新學(xué)習(xí)下Android,學(xué)習(xí)之前開(kāi)發(fā)環(huán)境的搭建是個(gè)首先要解決的問(wèn)題,所以下面這篇文章主要給大家介紹了Android開(kāi)發(fā)環(huán)境搭建的相關(guān)資料,文中將實(shí)現(xiàn)的步驟一步步介紹的非常詳細(xì),需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-11-11
android獲取屏幕高度和寬度的實(shí)現(xiàn)方法
這篇文章主要介紹了android獲取屏幕高度和寬度的實(shí)現(xiàn)方法,較為詳細(xì)的分析了Android獲取屏幕高度和寬度的原理與實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-01-01
Flutter進(jìn)階之實(shí)現(xiàn)動(dòng)畫效果(一)
這篇文章主要為大家詳細(xì)介紹了Flutter實(shí)現(xiàn)動(dòng)畫效果的第一篇,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08
React-Native Android 與 IOS App使用一份代碼實(shí)現(xiàn)方法
這篇文章主要介紹了React-Native Android 與 IOS App使用一份代碼實(shí)現(xiàn)方法的相關(guān)資料,這里舉例說(shuō)明,該如何實(shí)現(xiàn)IOS和Android APP 都使用一樣的代碼,需要的朋友可以參考下2016-12-12
Android解析json數(shù)組對(duì)象的方法及Apply和數(shù)組的三個(gè)技巧
這篇文章主要介紹了Android解析json數(shù)組對(duì)象的方法及Apply和數(shù)組的三個(gè)技巧的相關(guān)資料,需要的朋友可以參考下2015-12-12
淺談Android中關(guān)于靜態(tài)變量(static)的使用問(wèn)題
本文主要介紹了Android中關(guān)于靜態(tài)變量(static)的使用問(wèn)題,具有一定的參考作用,下面跟著小編一起來(lái)看下吧2017-01-01
Android實(shí)現(xiàn)底部切換標(biāo)簽
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)底部切換標(biāo)簽,嵌套Fragment,方便自定義布局,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07

