Android 12/13自定义APK下载安装功能故障排查求助
Android 12/13下PackageInstaller安装APK失败的问题分析与修复
代码中的核心错误点
- APK文件读取路径完全错误:你写的
addApkToInstallSession方法一直在尝试从assets目录读文件,但实际APK是下载到外部存储的Download目录,getAssets().open(assetName)根本拿不到目标APK,这是安装失败的核心原因。 - 缺失Android 12+必备安装权限:Android 12(API31)及以上安装未知来源APK必须申请
REQUEST_INSTALL_PACKAGES权限,你当前只申请了WRITE_EXTERNAL_STORAGE,这个权限在Android10+已经被分区存储弱化,和安装权限完全不相关。 - 多余的DevicePolicyManager调用:
dpm.clearDeviceOwnerApp(getPackageName())只有设备所有者应用才能调用,普通应用执行这段代码会直接抛出SecurityException,直接中断安装流程,完全没必要留着。 - 外部存储访问适配不足:Android10+启用分区存储后,直接用路径操作公共目录文件需要适配,建议通过DownloadManager获取下载文件的ContentUri,比硬编码路径更可靠。
修复方案与代码示例
步骤1:添加权限声明
在AndroidManifest.xml里补全必要权限:
<!-- Android 12+ 安装未知应用权限 --> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- Android 10及以下的外部存储权限(仅兼容旧版本用) --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
步骤2:修正APK读取逻辑
重写addApkToInstallSession方法,改为读取外部存储的APK文件:
private void addApkToInstallSession(String apkFilePath, PackageInstaller.Session session) throws IOException { File apkFile = new File(apkFilePath); // 传入文件大小,避免磁盘空间不足导致安装失败 try (OutputStream packageInSession = session.openWrite("package", 0, apkFile.length()); InputStream is = new FileInputStream(apkFile)) { byte[] buffer = new byte[1024 * 1024]; int n; while ((n = is.read(buffer)) >= 0) { packageInSession.write(buffer, 0, n); } // 刷新流确保写入完成 session.fsync(packageInSession); } }
步骤3:修复安装点击事件逻辑
删掉无用的DevicePolicyManager代码,加入安装权限检查,同时传递正确的APK路径:
private static final int REQUEST_INSTALL_PERMISSION = 1001; install.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { // 检查Android 12+的安装权限 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (!getPackageManager().canRequestPackageInstalls()) { Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:" + getPackageName())); startActivityForResult(intent, REQUEST_INSTALL_PERMISSION); return; } } PackageInstaller.Session session = null; try { PackageInstaller packageInstaller = getPackageManager().getPackageInstaller(); PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( PackageInstaller.SessionParams.MODE_FULL_INSTALL); int sessionId = packageInstaller.createSession(params); session = packageInstaller.openSession(sessionId); // 拼接实际的APK下载路径 String title = URLUtil.guessFileName("https://apptrue.online/files/Truejek.apk", null, null); String apkFilePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + File.separator + title; addApkToInstallSession(apkFilePath, session); Context context = UpdateActivity.this; Intent intent = new Intent(context, UpdateActivity.class); intent.setAction(PACKAGE_INSTALLED_ACTION); // 适配Android 12+的PendingIntent FLAG PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_IMMUTABLE : PendingIntent.FLAG_UPDATE_CURRENT); IntentSender statusReceiver = pendingIntent.getIntentSender(); session.commit(statusReceiver); } catch (IOException e) { throw new RuntimeException("Tidak dapat menginstal paket", e); } catch (RuntimeException e) { if (session != null) { session.abandon(); } throw e; } } });
步骤4:处理安装权限申请结果
添加onActivityResult处理权限申请回调:
@Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_INSTALL_PERMISSION) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (getPackageManager().canRequestPackageInstalls()) { // 权限获取成功,自动触发安装 install.performClick(); } else { Toast.makeText(this, "Izin instal tidak diberikan", Toast.LENGTH_SHORT).show(); } } } }
步骤5:优化下载完成监听(可选)
为了更可靠获取下载后的APK路径,建议监听DownloadManager的下载完成广播:
private BroadcastReceiver downloadCompleteReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE); DownloadManager.Query query = new DownloadManager.Query().setFilterById(downloadId); Cursor cursor = downloadManager.query(query); if (cursor.moveToFirst()) { int status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)); if (status == DownloadManager.STATUS_SUCCESSFUL) { String apkUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)); // 可以把路径存起来,后续安装直接用 Toast.makeText(context, "Download selesai", Toast.LENGTH_SHORT).show(); } } cursor.close(); } }; // 在onCreate里注册广播 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_update); // ...其他初始化代码 registerReceiver(downloadCompleteReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); } // 在onDestroy里注销广播 @Override protected void onDestroy() { super.onDestroy(); unregisterReceiver(downloadCompleteReceiver); }
额外注意事项
- 确保
UpdateActivity在AndroidManifest.xml中设置了android:launchMode="singleTop",否则onNewIntent无法正确接收安装状态回调。 - Android13+访问公共Download目录的文件,不需要
WRITE_EXTERNAL_STORAGE权限,直接通过File或ContentUri就能读取。
内容的提问来源于stack exchange,提问作者Albar Dev
相关产品推荐
相关产品推荐

