Android 下载安装APK踩坑纪录
前言:最近项目中需要有个静默下载,下载完成后弹框告诉用户是否下载的需求。开始觉得 so easy !,真的调研写demo的时候发现过程还是蛮曲折的。下面会详细述说。
注意:
- 待下载安装的 APK 一定要是签过名的,我当时是写 demo 嘛,所以上传到测试服务器的 APK是未签名的,结果无法安装。
- 写demo的项目中,下载安装的 APK 一定要是此 DEMO 项目中打包生成的签名 APK。我一开始图方便,直接去生成服务器上下载公司的 APK,结果下载完成后,安装没有任何效果,IDE 上也没有报错。
上面的注意点说完了,直接开始步骤了。
一、在 AndroidManifest.xml 中加入相应的权限:
<!--apk 安装 --><uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/><!--网络 --><uses-permission android:name="android.permission.INTERNET" />
由于我是 APK 直接下载到应用的目录,所以没申请 SD 卡的读写权限,这个大家自行考虑。
因为设计到下载,网络权限肯定不要忘记添加。
二、下载的代码如下:
public void downloadAPK(File apkFile) {//准备用于保存APK文件的File对象 : /storage/sdcard/Android/package_name/files/xxx.apkif (apkFile.exists()) {apkFile.delete();}// 开启子线程, 请求下载APK文件new Thread(() -> {try {long start = System.currentTimeMillis();//1. 得到连接对象String path = "https://xxx_pro_v3330105.apk";URL url = new URL(path);HttpURLConnection connection = (HttpURLConnection) url.openConnection();//2. 设置 connection.setConnectTimeout(30_000);connection.setReadTimeout(3_0000);//3. 连接connection.connect();//4. 请求并得到响应码200int responseCode = connection.getResponseCode();if (responseCode == 200) {//设置dialog的最大进度//5. 得到包含APK文件数据的InputStreamInputStream is = connection.getInputStream();//6. 创建指向apkFile的FileOutputStreamFileOutputStream fos = new FileOutputStream(apkFile);//7. 边读边写byte[] buffer = new byte[1024];int len = -1;int progress = 0;while ((len = is.read(buffer)) != -1) {fos.write(buffer, 0, len);progress += len;Log.e(TAG, " downloading progress = " + progress);}fos.close();is.close();}//9. 下载完成, 关闭connection.disconnect();long end = System.currentTimeMillis();Log.e(TAG, "--------------下载完成-------------" + (end - start));new Handler(Looper.getMainLooper()).post(new Runnable() {@Overridepublic void run() {Log.e(TAG, "--------------完成-------------" + (end - start));Toast.makeText(context, "下载完成,耗时:" + (end - start), Toast.LENGTH_LONG).show(); }});} catch (Exception e) {e.printStackTrace();}}).start();}
下载方法的测试代码如下:
public void testDownLoadApk() {// xxx_pro_v3330105.apk 表示生产 apk 的名称File apkFile = new File(context.getExternalFilesDir(null), "xxx_pro_v3330105.apk");downloadAPK(apkFile);}
下载没啥好说的了,遇到的坑都是安装的坑。
三、apk 安装
public void installAPK(File apkFile) {if (apkFile.exists() && apkFile.canRead()) {Intent intent = new Intent(Intent.ACTION_VIEW);// 读取uri权限intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);// 安卓7.0及以上,需要使用 FileProvider 来访问私有文件if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {Uri contentUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", apkFile);intent.setDataAndType(contentUri, "application/vnd.android.package-archive");} else {intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");}context.startActivity(intent);} else {Toast.makeText(context, "安装文件不存在", Toast.LENGTH_LONG).show();}}
由于需要使用 FileProvider ,所以我们需要在 AndroidManifest.xml
文件中进行声明:
<providerandroid:name="androidx.core.content.FileProvider"android:authorities="应用包名.fileprovider"android:grantUriPermissions="true"android:exported="false"><meta-dataandroid:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/file_path" /></provider>
注意: android:exported
要设置为 false. file_path
需要在 res
目录中 新建 xml
文件夹,然后新进此文件,如下图:
file_path
内容如下:
<paths><external-path path="Android/data/应用包名/" name="files_root" /><external-path path="." name="external_storage_root" />
</paths>
此时,运行安装代码,报错,如下:
为啥报这个错误呢,因为我引用的第3方SDK中也声明了 FileProvider
,怎么办呢?也很简单,自定义一个 FileProvider
,如下:
然后在 AndroidManifest 中使用自定义的 FileProvider
:
<providerandroid:name=".YKFileProvider"android:authorities="包名.fileprovider"android:grantUriPermissions="true"android:exported="false"><meta-dataandroid:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/file_path" /></provider>
此问题得以解决。最后也顺利下载安装完成。
注意,我这里 使用的是 androidx.core.content.FileProvider
。