Android实现Android APP自动更新(附带源码)

一、项目介绍

在移动应用的全生命周期中,版本迭代和用户更新体验至关重要。传统的做法是依赖 Google Play 商店强制推送更新,但在某些场景下,我们需要:

  • 更即时地控制更新流程(如灰度、强制升级、提醒升级等);

  • 支持市场外分发,比如企业内部应用分发、第三方应用商店;

  • 自定义更新 UI,与应用风格保持一致。

本项目示例将展示两种主流方案:

  1. Google Play In‑App Updates(官方方案,适用于上架 Play 商店的应用)

  2. 自建服务 + APK 下载 & 安装(适用于非 Play 分发场景)

通过本教程,你将学会如何在应用内检测新版本、弹出升级对话框、后台下载 APK、以及无缝触发安装流程,极大提升用户体验。


二、相关知识

  1. Google Play Core Library

    • com.google.android.play:core:1.x.x 包含了 In‑App Updates API,让应用可在运行时检查并触发“灵活更新”或“立即更新”流程,无需用户去 Play 商店界面。

  2. FileProvider & 安装意图

    • 对于自建更新方案,需要在 AndroidManifest.xml 配置 FileProvider,并通过 Intent.ACTION_VIEW 携带 APK 的 content:// URI,调用系统安装界面。

  3. WorkManager / DownloadManager

    • 长任务(如后台下载 APK)应使用 WorkManager 或系统 DownloadManager,保证下载可在后台稳定运行,且重启后可续传。

  4. 运行时权限 & 兼容性

    • Android 8.0+(API 26+)安装需获取 “允许安装未知应用” 权限 (REQUEST_INSTALL_PACKAGES)。

    • Android 7.0+(API 24+)文件 URI 必须走 FileProvider,否则会抛 FileUriExposedException


三、项目实现思路

  1. 版本检测

    • Play 方案:调用 Play Core 的 AppUpdateManager.getAppUpdateInfo() 检查更新状态。

    • 自建方案:向自有服务器发起网络请求(如 GET /latest_version.json),获取最新版本号、APK 下载地址、更新说明等。

  2. 弹窗交互

    • 根据策略选择“立即更新”(强制)或“灵活更新”(允许后台运行时再重启安装),并展示更新日志。

  3. 下载 APK

    • Play 方案:由 Play Core 自动下载。

    • 自建方案:用 DownloadManager 启动下载,并监听广播获取下载完成通知。

  4. 触发安装

    • 下载完成后,构造 Intent.ACTION_VIEW,指定 MIME 类型 application/vnd.android.package-archive,使用 FileProvider 共享 APK URI,启动安装流程。


四、完整代码(All‑in‑One,含详细注释)

// =======================================
// 文件: AutoUpdateManager.java + MainActivity
// (本示例将 Manager 与 Activity 合写于一处,注释区分)
// =======================================

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 {

    // ---------- 常量区 ----------
    private static final int REQUEST_CODE_UPDATE = 100;               // Play 更新请求码
    private static final int REQUEST_INSTALL_PERMISSION = 101;        // 动态安装权限
    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);  // 布局见下文

        // 按钮触发两种更新
        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 商店内更新
            }
        });

        btnCustomUpdate.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                checkCustomUpdate(); // 自建服务器更新
            }
        });

        // 初始化 DownloadManager
        downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
        // 注册下载完成广播
        registerReceiver(onDownloadComplete, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
    }

    // ---------- 1. Play In‑App Updates 检查 ----------
    private void checkPlayUpdate() {
        // 创建 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 {
                    // 发起灵活更新请求
                    appUpdateManager.startUpdateFlowForResult(
                            info,
                            AppUpdateType.FLEXIBLE,
                            this,
                            REQUEST_CODE_UPDATE);
                } catch (Exception e) {
                    Log.e(TAG, "Play 更新启动失败", e);
                }
            } else {
                Toast.makeText(this, "无可用更新或不支持此更新类型", Toast.LENGTH_SHORT).show();
            }
        });
    }

    // ---------- 2. 自建服务器版本检测 ----------
    private void checkCustomUpdate() {
        new Thread(() -> {
            try {
                // 1) 请求服务器 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) 获取本地版本号
                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. 弹出更新对话框 ----------
    private void showUpdateDialog(String apkUrl, String changeLog) {
        new AlertDialog.Builder(this)
            .setTitle("发现新版本")
            .setMessage(changeLog)
            .setCancelable(false)
            .setPositiveButton("立即更新", (dialog, which) -> {
                startDownload(apkUrl);
            })
            .setNegativeButton("稍后再说", null)
            .show();
    }

    // ---------- 4. 启动系统 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. 监听下载完成,触发安装 ----------
    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+ 需要请求安装未知应用权限
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                boolean canInstall = getPackageManager().canRequestPackageInstalls();
                if (!canInstall) {
                    // 请求“安装未知应用”权限
                    ActivityCompat.requestPermissions(MainActivity.this,
                        new String[]{Manifest.permission.REQUEST_INSTALL_PACKAGES},
                        REQUEST_INSTALL_PERMISSION);
                    return;
                }
            }
            installApk(apkFile);
        }
    };

    // ---------- 6. 处理未知来源权限申请结果 ----------
    @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) {
                // 再次触发安装(假设 APK 仍在下载目录)
                File apkFile = new File(Environment.getExternalStoragePublicDirectory(
                                Environment.DIRECTORY_DOWNLOADS), "update.apk");
                installApk(apkFile);
            } else {
                Toast.makeText(this, "安装权限被拒绝,无法自动更新", 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, "无法启动安装程序", Toast.LENGTH_LONG).show();
        }
    }

    // ---------- 8. Activity 销毁时注销 Receiver ----------
    @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(onDownloadComplete);
    }
}
<!-- ======================================
文件: AndroidManifest.xml
注意:需要配置 FileProvider 与权限
====================================== -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.autoupdate">

    <!-- 安装未知来源权限(Android 8.0+ 需动态申请) -->
    <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="自建服务更新"/>
</LinearLayout>

 

五、方法解读

  • checkPlayUpdate()
    检查 Google Play 上的更新可用性,并以“灵活更新”方式启动下载和安装流程。

  • checkCustomUpdate()
    通过 HttpURLConnection 请求服务器 JSON,解析最新 versionCodeapkUrl,对比本地版本,决定是否弹窗。

  • showUpdateDialog(...)
    基于服务器返回的 changeLog 构建 AlertDialog,提供“立即更新”与“稍后再说”两种交互。

  • startDownload(String apkUrl)
    使用系统 DownloadManager 发起后台下载,保存至公开目录,支持断点续传和系统下载通知。

  • BroadcastReceiver onDownloadComplete
    监听 DownloadManager.ACTION_DOWNLOAD_COMPLETE 广播,确认是本次下载后触发安装流程。

  • onRequestPermissionsResult(...)
    处理 Android 8.0+ “安装未知来源”权限授权结果,授权后继续调用 installApk()

  • installApk(File apkFile)
    通过 FileProvider 获取 APK 的 content URI,并以 Intent.ACTION_VIEW 调用系统安装器。


六、项目总结

优势

  • Play Core In‑App 更新:官方支持,体验与 Play 商店一致,无需手工管理下载逻辑。

  • 自建方案:灵活可控,支持任意分发渠道,自定义 UI 与灰度策略。

注意与优化

  1. 权限与兼容

    • Android 7.0+ 必须使用 FileProvider

    • Android 8.0+ 需动态申请 REQUEST_INSTALL_PACKAGES

  2. 下载失败重试

    • 可结合 WorkManager 增加重试与网络断线重连逻辑。

  3. 安全性

    • 建议对 APK 做签名校验(计算 SHA256 与服务器比对),防止被篡改。

  4. UI 体验

    • 对“立即更新”与“后台更新”作更多状态提示。

    • 可显示下载进度条、进度通知等。

  5. 灰度/强制升级

    • 可在服务器 JSON 中添加策略字段,如 forceUpdate,在对话框中禁止“稍后再说”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值