# Android 应用内 APK 安装方案从静默安装到普通安装gt; 场景TV 盒子、教育平板、企业 MDM 设备需要在应用内完成 APK 升级或第三方应用推送安装。gt; 已在 Android 4.4 ~ 12 的真机及模拟器上验证可直接用于生产环境。## 一、你的应用能走哪条路| 安装方式 | 所需权限 | 用户感知 | 适用场景 ||---------|---------|---------|---------|| **静默安装** | INSTALL_PACKAGES系统级 | 无弹窗后台完成 | 系统签名应用、厂商预装、Root 设备 || **普通安装** | REQUEST_INSTALL_PACKAGES动态申请 | 系统弹窗需手动确认 | 所有第三方应用 |**核心结论**- 如果你不是系统应用**直接看第三节普通安装即可**静默安装那部分了解就行。- Android 9.0 对 pm install 限制更严非系统 UID 会直接拒绝。---## 二、权限声明AndroidManifest.xmlxml!-- 静默安装仅限系统应用普通应用加了也没用 --uses-permissionandroid:nameandroid.permission.INSTALL_PACKAGEStools:ignoreProtectedPermissions /!-- 普通安装Android 8.0API 26 必须声明 --uses-permission android:nameandroid.permission.REQUEST_INSTALL_PACKAGES /## 三、核心工具类完整生产代码public class ApkInstallUtil {private static final String TAG ApkInstallUtil;/*** 统一入口优先静默安装无权限则降级为普通安装*/public static void installApp(Context context, String filePath) {if (context null || TextUtils.isEmpty(filePath)) {AppUtil.toast(安装参数异常);return;}try {// 只有系统应用才能拿到 INSTALL_PACKAGES 权限if (ContextCompat.checkSelfPermission(context, Manifest.permission.INSTALL_PACKAGES) PackageManager.PERMISSION_GRANTED) {silentInstall(context, filePath);} else {normalInstall(context, filePath);}} catch (Exception e) {normalInstall(context, filePath);}}/* 静默安装区域系统应用专用 */private static void silentInstall(Context context, String filePath) throws Exception {if (Build.VERSION.SDK_INT Build.VERSION_CODES.LOLLIPOP) {installApkByPm(filePath);} else {installApkBySession(context, filePath);}}/*** Android 5.0 之前通过 pm install 命令安装*/private static boolean installApkByPm(String apkPath) {String[] args {pm, install, -r, apkPath};ProcessBuilder processBuilder new ProcessBuilder(args);Process process null;BufferedReader successResult null;BufferedReader errorResult null;StringBuilder successMsg new StringBuilder();StringBuilder errorMsg new StringBuilder();try {process processBuilder.start();successResult new BufferedReader(new InputStreamReader(process.getInputStream()));errorResult new BufferedReader(new InputStreamReader(process.getErrorStream()));String s;while ((s successResult.readLine()) ! null) {successMsg.append(s);}while ((s errorResult.readLine()) ! null) {errorMsg.append(s);}return process.waitFor() 0 || successMsg.toString().contains(Success);} catch (IOException | InterruptedException e) {e.printStackTrace();} finally {closeQuietly(successResult);closeQuietly(errorResult);if (process ! null) {process.destroy();}}return false;}/*** Android 5.0通过 PackageInstaller API 安装*/RequiresApi(api Build.VERSION_CODES.LOLLIPOP)private static void installApkBySession(Context context, String apkFilePath) throws Exception {PackageManager packageManager context.getPackageManager();File apkFile new File(apkFilePath);PackageInstaller packageInstaller packageManager.getPackageInstaller();if (packageInstaller null) return;PackageInstaller.SessionParams sessionParams new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);sessionParams.setSize(apkFile.length());int sessionId createSession(packageInstaller, sessionParams);new Thread(() - {Looper.prepare();packageInstaller.registerSessionCallback(new InstallSessionCallback(sessionId));if (sessionId ! -1) {boolean copySuccess copyInstallFile(packageInstaller, sessionId, apkFilePath);if (copySuccess) {execInstallCommand(context, packageInstaller, sessionId);}}Looper.loop(); // 必须 loop否则 SessionCallback 无法回调}).start();}RequiresApi(api Build.VERSION_CODES.LOLLIPOP)private static int createSession(PackageInstaller packageInstaller,PackageInstaller.SessionParams sessionParams) {try {return packageInstaller.createSession(sessionParams);} catch (IOException e) {e.printStackTrace();return -1;}}RequiresApi(api Build.VERSION_CODES.LOLLIPOP)private static boolean copyInstallFile(PackageInstaller packageInstaller,int sessionId, String apkFilePath) {InputStream in null;OutputStream out null;PackageInstaller.Session session null;try {File apkFile new File(apkFilePath);session packageInstaller.openSession(sessionId);out session.openWrite(base.apk, 0, apkFile.length());in new FileInputStream(apkFile);int c;byte[] buffer new byte[65536];while ((c in.read(buffer)) ! -1) {out.write(buffer, 0, c);}session.fsync(out);return true;} catch (IOException e) {e.printStackTrace();return false;} finally {closeQuietly(out);closeQuietly(in);closeQuietly(session);}}RequiresApi(api Build.VERSION_CODES.LOLLIPOP)private static void execInstallCommand(Context context,PackageInstaller packageInstaller, int sessionId) {PackageInstaller.Session session null;try {session packageInstaller.openSession(sessionId);Intent intent new Intent(context, InstallReceiver.class);// Android 12 必须加 FLAG_IMMUTABLE否则抛 IllegalArgumentExceptionint flags PendingIntent.FLAG_UPDATE_CURRENT;if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) {flags | PendingIntent.FLAG_IMMUTABLE;}PendingIntent pendingIntent PendingIntent.getBroadcast(context, 1, intent, flags);session.commit(pendingIntent.getIntentSender());} catch (IOException e) {e.printStackTrace();} finally {closeQuietly(session);}}RequiresApi(api Build.VERSION_CODES.LOLLIPOP)public static class InstallSessionCallback extends PackageInstaller.SessionCallback {private final int mSessionId;public InstallSessionCallback(int sessionId) {this.mSessionId sessionId;}Overridepublic void onCreated(int sessionId) {}Overridepublic void onBadgingChanged(int sessionId) {}Overridepublic void onActiveChanged(int sessionId, boolean active) {}Overridepublic void onProgressChanged(int sessionId, float progress) {if (sessionId mSessionId) {LogUtil.e(TAG, 安装进度: progress);}}Overridepublic void onFinished(int sessionId, boolean success) {if (mSessionId sessionId) {LogUtil.e(TAG, success ? 静默安装成功 : 静默安装失败);}}}/* 普通安装区域所有应用可用 *//*** 普通安装系统弹窗用户手动确认*/public static void normalInstall(Context context, String filePath) {File file new File(filePath);if (!file.exists()) {AppUtil.toast(安装包不存在);return;}// Android 8.0 必须先检查是否有安装未知应用权限if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) {boolean canInstall context.getPackageManager().canRequestPackageInstalls();if (!canInstall) {// 引导用户去设置页开启权限Intent settingIntent new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).setData(Uri.parse(package: context.getPackageName()));context.startActivity(settingIntent);return;}}Intent intent new Intent(Intent.ACTION_VIEW);intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);if (Build.VERSION.SDK_INT Build.VERSION_CODES.N) {// 7.0 必须使用 FileProvider直接传 File URI 会抛 FileUriExposedExceptionintent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);Uri contentUri FileProvider.getUriForFile(context,BuildConfig.APPLICATION_ID .fileProvider, file);intent.setDataAndType(contentUri, application/vnd.android.package-archive);} else {intent.setDataAndType(Uri.fromFile(file), application/vnd.android.package-archive);}// 注册安装完成监听用于安装成功后删除安装包InstallCompleteReceiver receiver new InstallCompleteReceiver(file);IntentFilter filter new IntentFilter(Intent.ACTION_PACKAGE_ADDED);filter.addDataScheme(package);context.registerReceiver(receiver, filter);try {context.startActivity(intent);} catch (Throwable e) {AppUtil.toast(安装出错或无权限);LogUtil.e(TAG, normalInstall error: e);// 启动失败时立即注销广播防止内存泄漏try {context.unregisterReceiver(receiver);} catch (Exception ignored) {}}}/*** 安装完成广播匹配包名后删除安装包并注销自身*/public static class InstallCompleteReceiver extends BroadcastReceiver {private final File apkFile;public InstallCompleteReceiver(File apkFile) {this.apkFile apkFile;}Overridepublic void onReceive(Context context, Intent intent) {String packageName intent.getData().getSchemeSpecificPart();String installingPkg getPackageName(context, apkFile.getAbsolutePath());if (packageName ! null packageName.equals(installingPkg)) {try {if (apkFile.exists()) apkFile.delete();} catch (Exception e) {LogUtil.e(TAG, 删除安装包失败: e.getMessage());} finally {try {context.unregisterReceiver(this);} catch (Exception ignored) {}}}}}/* 公共工具方法 *//*** 通过 APK 文件路径解析包名*/public static String getPackageName(Context context, String filePath) {PackageManager pm context.getPackageManager();PackageInfo info pm.getPackageArchiveInfo(filePath, PackageManager.GET_ACTIVITIES);return info ! null ? info.applicationInfo.packageName : null;}private static void closeQuietly(Closeable c) {if (c ! null) {try {c.close();} catch (IOException ignored) {}}}}## 四、FileProvider 配置普通安装必备Android 7.0注意直接传 Uri.fromFile() 在 7.0 会直接崩溃。**AndroidManifest.xml**providerandroid:nameandroidx.core.content.FileProviderandroid:authorities${applicationId}.fileProviderandroid:exportedfalseandroid:grantUriPermissionstruemeta-dataandroid:nameandroid.support.FILE_PROVIDER_PATHSandroid:resourcexml/file_paths //provider**res/xml/file_paths.xml**?xml version1.0 encodingutf-8?pathsexternal-path nameexternal_files path. /cache-path namecache_files path. /!-- 根据你实际存放 APK 的路径补充例如 --files-path nameinternal_files path. //paths## 五、总结1. **先判断身份**不是系统应用就不要折腾静默安装直接走普通安装。1. **Android 7.0 必须用 FileProvider**。1. **Android 8.0 普通安装前必须检查 canRequestPackageInstalls()** 否则 startActivity 没反应。1. **Android 12 的 PendingIntent 必须加 FLAG_IMMUTABLE**否则直接崩溃。1. **PackageInstaller 的 SessionCallback 必须跑在带 Looper 的线程里**否则安装完成了也收不到回调。
Android 应用内 APK 安装方案:从静默安装到普通安装
# Android 应用内 APK 安装方案从静默安装到普通安装gt; 场景TV 盒子、教育平板、企业 MDM 设备需要在应用内完成 APK 升级或第三方应用推送安装。gt; 已在 Android 4.4 ~ 12 的真机及模拟器上验证可直接用于生产环境。## 一、你的应用能走哪条路| 安装方式 | 所需权限 | 用户感知 | 适用场景 ||---------|---------|---------|---------|| **静默安装** | INSTALL_PACKAGES系统级 | 无弹窗后台完成 | 系统签名应用、厂商预装、Root 设备 || **普通安装** | REQUEST_INSTALL_PACKAGES动态申请 | 系统弹窗需手动确认 | 所有第三方应用 |**核心结论**- 如果你不是系统应用**直接看第三节普通安装即可**静默安装那部分了解就行。- Android 9.0 对 pm install 限制更严非系统 UID 会直接拒绝。---## 二、权限声明AndroidManifest.xmlxml!-- 静默安装仅限系统应用普通应用加了也没用 --uses-permissionandroid:nameandroid.permission.INSTALL_PACKAGEStools:ignoreProtectedPermissions /!-- 普通安装Android 8.0API 26 必须声明 --uses-permission android:nameandroid.permission.REQUEST_INSTALL_PACKAGES /## 三、核心工具类完整生产代码public class ApkInstallUtil {private static final String TAG ApkInstallUtil;/*** 统一入口优先静默安装无权限则降级为普通安装*/public static void installApp(Context context, String filePath) {if (context null || TextUtils.isEmpty(filePath)) {AppUtil.toast(安装参数异常);return;}try {// 只有系统应用才能拿到 INSTALL_PACKAGES 权限if (ContextCompat.checkSelfPermission(context, Manifest.permission.INSTALL_PACKAGES) PackageManager.PERMISSION_GRANTED) {silentInstall(context, filePath);} else {normalInstall(context, filePath);}} catch (Exception e) {normalInstall(context, filePath);}}/* 静默安装区域系统应用专用 */private static void silentInstall(Context context, String filePath) throws Exception {if (Build.VERSION.SDK_INT Build.VERSION_CODES.LOLLIPOP) {installApkByPm(filePath);} else {installApkBySession(context, filePath);}}/*** Android 5.0 之前通过 pm install 命令安装*/private static boolean installApkByPm(String apkPath) {String[] args {pm, install, -r, apkPath};ProcessBuilder processBuilder new ProcessBuilder(args);Process process null;BufferedReader successResult null;BufferedReader errorResult null;StringBuilder successMsg new StringBuilder();StringBuilder errorMsg new StringBuilder();try {process processBuilder.start();successResult new BufferedReader(new InputStreamReader(process.getInputStream()));errorResult new BufferedReader(new InputStreamReader(process.getErrorStream()));String s;while ((s successResult.readLine()) ! null) {successMsg.append(s);}while ((s errorResult.readLine()) ! null) {errorMsg.append(s);}return process.waitFor() 0 || successMsg.toString().contains(Success);} catch (IOException | InterruptedException e) {e.printStackTrace();} finally {closeQuietly(successResult);closeQuietly(errorResult);if (process ! null) {process.destroy();}}return false;}/*** Android 5.0通过 PackageInstaller API 安装*/RequiresApi(api Build.VERSION_CODES.LOLLIPOP)private static void installApkBySession(Context context, String apkFilePath) throws Exception {PackageManager packageManager context.getPackageManager();File apkFile new File(apkFilePath);PackageInstaller packageInstaller packageManager.getPackageInstaller();if (packageInstaller null) return;PackageInstaller.SessionParams sessionParams new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);sessionParams.setSize(apkFile.length());int sessionId createSession(packageInstaller, sessionParams);new Thread(() - {Looper.prepare();packageInstaller.registerSessionCallback(new InstallSessionCallback(sessionId));if (sessionId ! -1) {boolean copySuccess copyInstallFile(packageInstaller, sessionId, apkFilePath);if (copySuccess) {execInstallCommand(context, packageInstaller, sessionId);}}Looper.loop(); // 必须 loop否则 SessionCallback 无法回调}).start();}RequiresApi(api Build.VERSION_CODES.LOLLIPOP)private static int createSession(PackageInstaller packageInstaller,PackageInstaller.SessionParams sessionParams) {try {return packageInstaller.createSession(sessionParams);} catch (IOException e) {e.printStackTrace();return -1;}}RequiresApi(api Build.VERSION_CODES.LOLLIPOP)private static boolean copyInstallFile(PackageInstaller packageInstaller,int sessionId, String apkFilePath) {InputStream in null;OutputStream out null;PackageInstaller.Session session null;try {File apkFile new File(apkFilePath);session packageInstaller.openSession(sessionId);out session.openWrite(base.apk, 0, apkFile.length());in new FileInputStream(apkFile);int c;byte[] buffer new byte[65536];while ((c in.read(buffer)) ! -1) {out.write(buffer, 0, c);}session.fsync(out);return true;} catch (IOException e) {e.printStackTrace();return false;} finally {closeQuietly(out);closeQuietly(in);closeQuietly(session);}}RequiresApi(api Build.VERSION_CODES.LOLLIPOP)private static void execInstallCommand(Context context,PackageInstaller packageInstaller, int sessionId) {PackageInstaller.Session session null;try {session packageInstaller.openSession(sessionId);Intent intent new Intent(context, InstallReceiver.class);// Android 12 必须加 FLAG_IMMUTABLE否则抛 IllegalArgumentExceptionint flags PendingIntent.FLAG_UPDATE_CURRENT;if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) {flags | PendingIntent.FLAG_IMMUTABLE;}PendingIntent pendingIntent PendingIntent.getBroadcast(context, 1, intent, flags);session.commit(pendingIntent.getIntentSender());} catch (IOException e) {e.printStackTrace();} finally {closeQuietly(session);}}RequiresApi(api Build.VERSION_CODES.LOLLIPOP)public static class InstallSessionCallback extends PackageInstaller.SessionCallback {private final int mSessionId;public InstallSessionCallback(int sessionId) {this.mSessionId sessionId;}Overridepublic void onCreated(int sessionId) {}Overridepublic void onBadgingChanged(int sessionId) {}Overridepublic void onActiveChanged(int sessionId, boolean active) {}Overridepublic void onProgressChanged(int sessionId, float progress) {if (sessionId mSessionId) {LogUtil.e(TAG, 安装进度: progress);}}Overridepublic void onFinished(int sessionId, boolean success) {if (mSessionId sessionId) {LogUtil.e(TAG, success ? 静默安装成功 : 静默安装失败);}}}/* 普通安装区域所有应用可用 *//*** 普通安装系统弹窗用户手动确认*/public static void normalInstall(Context context, String filePath) {File file new File(filePath);if (!file.exists()) {AppUtil.toast(安装包不存在);return;}// Android 8.0 必须先检查是否有安装未知应用权限if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) {boolean canInstall context.getPackageManager().canRequestPackageInstalls();if (!canInstall) {// 引导用户去设置页开启权限Intent settingIntent new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).setData(Uri.parse(package: context.getPackageName()));context.startActivity(settingIntent);return;}}Intent intent new Intent(Intent.ACTION_VIEW);intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);if (Build.VERSION.SDK_INT Build.VERSION_CODES.N) {// 7.0 必须使用 FileProvider直接传 File URI 会抛 FileUriExposedExceptionintent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);Uri contentUri FileProvider.getUriForFile(context,BuildConfig.APPLICATION_ID .fileProvider, file);intent.setDataAndType(contentUri, application/vnd.android.package-archive);} else {intent.setDataAndType(Uri.fromFile(file), application/vnd.android.package-archive);}// 注册安装完成监听用于安装成功后删除安装包InstallCompleteReceiver receiver new InstallCompleteReceiver(file);IntentFilter filter new IntentFilter(Intent.ACTION_PACKAGE_ADDED);filter.addDataScheme(package);context.registerReceiver(receiver, filter);try {context.startActivity(intent);} catch (Throwable e) {AppUtil.toast(安装出错或无权限);LogUtil.e(TAG, normalInstall error: e);// 启动失败时立即注销广播防止内存泄漏try {context.unregisterReceiver(receiver);} catch (Exception ignored) {}}}/*** 安装完成广播匹配包名后删除安装包并注销自身*/public static class InstallCompleteReceiver extends BroadcastReceiver {private final File apkFile;public InstallCompleteReceiver(File apkFile) {this.apkFile apkFile;}Overridepublic void onReceive(Context context, Intent intent) {String packageName intent.getData().getSchemeSpecificPart();String installingPkg getPackageName(context, apkFile.getAbsolutePath());if (packageName ! null packageName.equals(installingPkg)) {try {if (apkFile.exists()) apkFile.delete();} catch (Exception e) {LogUtil.e(TAG, 删除安装包失败: e.getMessage());} finally {try {context.unregisterReceiver(this);} catch (Exception ignored) {}}}}}/* 公共工具方法 *//*** 通过 APK 文件路径解析包名*/public static String getPackageName(Context context, String filePath) {PackageManager pm context.getPackageManager();PackageInfo info pm.getPackageArchiveInfo(filePath, PackageManager.GET_ACTIVITIES);return info ! null ? info.applicationInfo.packageName : null;}private static void closeQuietly(Closeable c) {if (c ! null) {try {c.close();} catch (IOException ignored) {}}}}## 四、FileProvider 配置普通安装必备Android 7.0注意直接传 Uri.fromFile() 在 7.0 会直接崩溃。**AndroidManifest.xml**providerandroid:nameandroidx.core.content.FileProviderandroid:authorities${applicationId}.fileProviderandroid:exportedfalseandroid:grantUriPermissionstruemeta-dataandroid:nameandroid.support.FILE_PROVIDER_PATHSandroid:resourcexml/file_paths //provider**res/xml/file_paths.xml**?xml version1.0 encodingutf-8?pathsexternal-path nameexternal_files path. /cache-path namecache_files path. /!-- 根据你实际存放 APK 的路径补充例如 --files-path nameinternal_files path. //paths## 五、总结1. **先判断身份**不是系统应用就不要折腾静默安装直接走普通安装。1. **Android 7.0 必须用 FileProvider**。1. **Android 8.0 普通安装前必须检查 canRequestPackageInstalls()** 否则 startActivity 没反应。1. **Android 12 的 PendingIntent 必须加 FLAG_IMMUTABLE**否则直接崩溃。1. **PackageInstaller 的 SessionCallback 必须跑在带 Looper 的线程里**否则安装完成了也收不到回调。