Android實(shí)現(xiàn)Android?APP自動(dòng)更新功能
一、項(xiàng)目介紹
在移動(dòng)應(yīng)用的全生命周期中,版本迭代和用戶更新體驗(yàn)至關(guān)重要。傳統(tǒng)的做法是依賴 Google Play 商店強(qiáng)制推送更新,但在某些場景下,我們需要:
更即時(shí)地控制更新流程(如灰度、強(qiáng)制升級(jí)、提醒升級(jí)等);
支持市場外分發(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ā)場景)
通過本教程,你將學(xué)會(huì)如何在應(yīng)用內(nèi)檢測新版本、彈出升級(jí)對(duì)話框、后臺(tái)下載 APK、以及無縫觸發(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ā)“靈活更新”或“立即更新”流程,無需用戶去 Play 商店界面。
FileProvider & 安裝意圖
對(duì)于自建更新方案,需要在
AndroidManifest.xml
配置FileProvider
,并通過Intent.ACTION_VIEW
攜帶 APK 的content://
URI,調(diào)用系統(tǒng)安裝界面。
WorkManager / DownloadManager
長任務(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)思路
版本檢測
Play 方案:調(diào)用 Play Core 的
AppUpdateManager.getAppUpdateInfo()
檢查更新狀態(tài)。自建方案:向自有服務(wù)器發(fā)起網(wǎng)絡(luò)請(qǐng)求(如 GET
/latest_version.json
),獲取最新版本號(hào)、APK 下載地址、更新說明等。
彈窗交互
根據(jù)策略選擇“立即更新”(強(qiáng)制)或“靈活更新”(允許后臺(tái)運(yùn)行時(shí)再重啟安裝),并展示更新日志。
下載 APK
Play 方案:由 Play Core 自動(dòng)下載。
自建方案:用
DownloadManager
啟動(dòng)下載,并監(jiān)聽廣播獲取下載完成通知。
觸發(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 庫依賴(立即更新/靈活更新) // 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); // 布局見下文 // 按鈕觸發(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, "無可用更新或不支持此更新類型", Toast.LENGTH_SHORT).show(); } }); } // ---------- 2. 自建服務(wù)器版本檢測 ---------- 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("稍后再說", 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"); // 開始下載 downloadId = downloadManager.enqueue(request); } // ---------- 5. 監(jiān)聽下載完成,觸發(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. 處理未知來源權(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)限被拒絕,無法自動(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, "無法啟動(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"> <!-- 安裝未知來源權(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 簡單示例界面 ====================================== --> <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()
通過HttpURLConnection
請(qǐng)求服務(wù)器 JSON,解析最新versionCode
與apkUrl
,對(duì)比本地版本,決定是否彈窗。showUpdateDialog(...)
基于服務(wù)器返回的changeLog
構(gòu)建AlertDialog
,提供“立即更新”與“稍后再說”兩種交互。startDownload(String apkUrl)
使用系統(tǒng)DownloadManager
發(fā)起后臺(tái)下載,保存至公開目錄,支持?jǐn)帱c(diǎn)續(xù)傳和系統(tǒng)下載通知。BroadcastReceiver onDownloadComplete
監(jiān)聽DownloadManager.ACTION_DOWNLOAD_COMPLETE
廣播,確認(rèn)是本次下載后觸發(fā)安裝流程。onRequestPermissionsResult(...)
處理 Android 8.0+ “安裝未知來源”權(quán)限授權(quán)結(jié)果,授權(quán)后繼續(xù)調(diào)用installApk()
。installApk(File apkFile)
通過FileProvider
獲取 APK 的 content URI,并以Intent.ACTION_VIEW
調(diào)用系統(tǒng)安裝器。
六、項(xiàng)目總結(jié)
優(yōu)勢(shì)
Play Core In?App 更新:官方支持,體驗(yàn)與 Play 商店一致,無需手工管理下載邏輯。
自建方案:靈活可控,支持任意分發(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ì)話框中禁止“稍后再說”。
到此這篇關(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)容保存為圖像的簡單實(shí)例
這篇文章主要介紹了Android中將View的內(nèi)容保存為圖像的簡單實(shí)例,有需要的朋友可以參考一下2014-01-01Android開發(fā)入門環(huán)境快速搭建實(shí)戰(zhàn)教程
最近想重新學(xué)習(xí)下Android,學(xué)習(xí)之前開發(fā)環(huán)境的搭建是個(gè)首先要解決的問題,所以下面這篇文章主要給大家介紹了Android開發(fā)環(huán)境搭建的相關(guān)資料,文中將實(shí)現(xiàn)的步驟一步步介紹的非常詳細(xì),需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-11-11android獲取屏幕高度和寬度的實(shí)現(xiàn)方法
這篇文章主要介紹了android獲取屏幕高度和寬度的實(shí)現(xiàn)方法,較為詳細(xì)的分析了Android獲取屏幕高度和寬度的原理與實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-01-01Flutter進(jìn)階之實(shí)現(xiàn)動(dòng)畫效果(一)
這篇文章主要為大家詳細(xì)介紹了Flutter實(shí)現(xiàn)動(dòng)畫效果的第一篇,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08React-Native Android 與 IOS App使用一份代碼實(shí)現(xiàn)方法
這篇文章主要介紹了React-Native Android 與 IOS App使用一份代碼實(shí)現(xiàn)方法的相關(guān)資料,這里舉例說明,該如何實(shí)現(xiàn)IOS和Android APP 都使用一樣的代碼,需要的朋友可以參考下2016-12-12Android解析json數(shù)組對(duì)象的方法及Apply和數(shù)組的三個(gè)技巧
這篇文章主要介紹了Android解析json數(shù)組對(duì)象的方法及Apply和數(shù)組的三個(gè)技巧的相關(guān)資料,需要的朋友可以參考下2015-12-12淺談Android中關(guān)于靜態(tài)變量(static)的使用問題
本文主要介紹了Android中關(guān)于靜態(tài)變量(static)的使用問題,具有一定的參考作用,下面跟著小編一起來看下吧2017-01-01Android實(shí)現(xiàn)底部切換標(biāo)簽
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)底部切換標(biāo)簽,嵌套Fragment,方便自定義布局,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07