前言:最近项目中需要有个静默下载,下载完成后弹框告诉用户是否下载的需求。开始觉得 so easy !,真的调研写demo的时候发现过程还是蛮曲折的。下面会详细述说。
注意:
上面的注意点说完了,直接开始步骤了。
<!--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.apk
if (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. 请求并得到响应码200
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
//设置dialog的最大进度
//5. 得到包含APK文件数据的InputStream
InputStream is = connection.getInputStream();
//6. 创建指向apkFile的FileOutputStream
FileOutputStream 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() {
@Override
public 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);
}
下载没啥好说的了,遇到的坑都是安装的坑。
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
文件中进行声明:
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="应用包名.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android: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
:
<provider
android:name=".YKFileProvider"
android:authorities="包名.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_path" />
</provider>
此问题得以解决。最后也顺利下载安装完成。
注意,我这里 使用的是 androidx.core.content.FileProvider
。