FileProvider安装应用
一、权限相关
1、“未知来源”应用
ndroid8.0及其以上系统,为了申请“未知来源”,需要在清单文件添加权限
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
然后代码中动态申请
public void setInstallPermission(){
boolean haveInstallPermission;
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
//先判断是否有安装未知来源应用的权限
haveInstallPermission = getPackageManager().canRequestPackageInstalls();
if(!haveInstallPermission){
//弹框提示用户手动打开
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("安装权限");
builder.setMessage("需要打开允许来自此来源,请去设置中开启此权限");
builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//此方法需要API>=26才能使用
toInstallPermissionSettingIntent();
}
}
});
builder.setCancelable(false);
builder.setIcon(R.mipmap.ic_launcher);
AlertDialog dialog = builder.create();
dialog.show();
}
}
}
/**
* 开启安装未知来源权限
*/
@RequiresApi(api = Build.VERSION_CODES.O)
private void toInstallPermissionSettingIntent() {
Uri packageURI = Uri.parse("package:"+getPackageName());
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,packageURI);
startActivityForResult(intent, INSTALL_PERMISS_CODE);
}
2、访问存储权限
当然文件读写权限也需要的,在清单文件添加权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
然后代码中动态申请
private void grantPermission() {
if (ContextCompat.checkSelfPermission(this,Manifest.permission.READ_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED
|| ContextCompat.checkSelfPermission(this,Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.WRITE_EXTERNAL_STORAGE},STORAGE_PERMISS_CODE);
}
}
二、FileProvider使用
android7.0以及以后,谷歌为了安全考虑,禁止应用明文跨进程访问uri,FileProvider应运而生。
老版本可能会用到v4兼容包,在AndroidManife.xml中使用android.support.v4.content.FileProvider,同时build.gradle中添加相关依赖,比如下面
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'com.android.support:recyclerview-v7:27.1.1'
implementation 'com.android.support:exifinterface:27.1.1'
implementation 'com.android.support:support-v4:27.1.1'
android X的话使用androidx.core.content.FileProvider,后续以android X为例吧。AndroidManife.xml中添加FileProvider如下
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.shan.fileprovider.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
provider_paths.xml中内容是
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!--根节点/-->
<root-path name="root" path="." />
<!--相当于Context.getFilesDir()-->
<files-path name="files" path="." />
<!--相当于Context. getCacheDir()-->
<cache-path name="cache" path="." />
<!--相当于Environment.getExternalStorageDirectory()-->
<external-path name="external" path="." />
<!--相当于Context.getExternalFilesDir(String) Context.getExternalFilesDir(null).-->
<external-files-path name="external_files" path="." />
<!--相当于Context.getExternalCacheDir()-->
<external-cache-path name="external_cache" path="." />
<!--相当于Context.getExternalMediaDirs()-->
<external-media-path name="name" path="." />
</paths>
上面的provider_paths.xml应该是比较万能了。
然后代码中调用安装程序
private void installApp(Context content) {
File fileS = new File("/storage/emulated/0/aa/apks/UU加速器.apk");
// File fileS = new File("/storage/emulated/0/Android/data/com.shan.fileprovider/files/aa/apks/UU加速器.apk");
// File fileS = new File("data/data/com.shan.fileprovider/files/aa/apks/UU加速器.apk");
Log.d(TAG, "installAp,"+fileS+",, exist="+fileS.exists());
Uri data;
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
// 给目标应用一个临时授权
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
data = FileProvider.getUriForFile(content, BuildConfig.APPLICATION_ID + ".fileprovider", fileS);
Log.d(TAG, "installApp: 1");
} else {
data = Uri.fromFile(fileS);
Log.d(TAG, "installApp: 11");
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(data, "application/vnd.android.package-archive");
startActivity(intent);
}
三、踩坑
如果安装时跳出“解析软件包出现问题”,那么可能就相应路径下的apk文件不存在,或者没有声明STORAGE的读写权限。
如果程序直接挂掉,并且打印
Caused by: java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/0/aa/apks/UU加速器.apk
at androidx.core.content.FileProvider$SimplePathStrategy.getUriForFile(FileProvider.java:744)
at androidx.core.content.FileProvider.getUriForFile(FileProvider.java:418)
at com.shan.fileprovider.MainActivity.installApp(MainActivity.java:57)
此时应该是你的provider_paths.xml路径配错了,可以看到是FileProvider的getUriForFile方法报错,跟进FileProvider.java源码看到
public Uri getUriForFile(File file) {
String path;
try {
path = file.getCanonicalPath();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
}
// Find the most-specific root path
Map.Entry<String, File> mostSpecific = null;
for (Map.Entry<String, File> root : mRoots.entrySet()) {
final String rootPath = root.getValue().getPath();
if (path.startsWith(rootPath) && (mostSpecific == null
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
mostSpecific = root; //1处报错,说明mostSpecific为null,那么肯定是path.startsWith(rootPath)不满足条件导致
}
}
if (mostSpecific == null) {
throw new IllegalArgumentException(
"Failed to find configured root that contains " + path); //1 这里报错
}
// Start at first char of path under root
final String rootPath = mostSpecific.getValue().getPath();
if (rootPath.endsWith("/")) {
path = path.substring(rootPath.length());
} else {
path = path.substring(rootPath.length() + 1);
}
// Encode the tag and path separately
path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
return new Uri.Builder().scheme("content")
.authority(mAuthority).encodedPath(path).build();
}
path.startsWith(rootPath)为什么为false呢?
path是文件实际路径,rootPath是mRoots遍历得到,rootPath实际上是provider_paths.xml配置的路径path的路径,可以从FileProvider的parsePathStrategy方法中找到mRoots赋值地方。
private static final File DEVICE_ROOT = new File("/");
private static final String TAG_ROOT_PATH = "root-path";
private static final String TAG_FILES_PATH = "files-path";
private static final String TAG_CACHE_PATH = "cache-path";
private static final String TAG_EXTERNAL = "external-path";
private static final String TAG_EXTERNAL_FILES = "external-files-path";
private static final String TAG_EXTERNAL_CACHE = "external-cache-path";
private static final String TAG_EXTERNAL_MEDIA = "external-media-path";
private static PathStrategy parsePathStrategy(Context context, String authority)
throws IOException, XmlPullParserException {
final SimplePathStrategy strat = new SimplePathStrategy(authority);
final ProviderInfo info = context.getPackageManager()
.resolveContentProvider(authority, PackageManager.GET_META_DATA);
if (info == null) {
throw new IllegalArgumentException(
"Couldn't find meta-data for provider with authority " + authority);
}
final XmlResourceParser in = info.loadXmlMetaData(
context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
if (in == null) {
throw new IllegalArgumentException(
"Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data");
}
int type;
while ((type = in.next()) != END_DOCUMENT) { //遍历path.xml中定义的节点
if (type == START_TAG) {
final String tag = in.getName();
final String name = in.getAttributeValue(null, ATTR_NAME);
String path = in.getAttributeValue(null, ATTR_PATH);
File target = null;
if (TAG_ROOT_PATH.equals(tag)) {
target = DEVICE_ROOT;
} else if (TAG_FILES_PATH.equals(tag)) {
target = context.getFilesDir();
} else if (TAG_CACHE_PATH.equals(tag)) {
target = context.getCacheDir();
} else if (TAG_EXTERNAL.equals(tag)) {
target = Environment.getExternalStorageDirectory();
} else if (TAG_EXTERNAL_FILES.equals(tag)) {
File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null);
if (externalFilesDirs.length > 0) {
target = externalFilesDirs[0];
}
} else if (TAG_EXTERNAL_CACHE.equals(tag)) {
File[] externalCacheDirs = ContextCompat.getExternalCacheDirs(context);
if (externalCacheDirs.length > 0) {
target = externalCacheDirs[0];
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& TAG_EXTERNAL_MEDIA.equals(tag)) {
File[] externalMediaDirs = context.getExternalMediaDirs();
if (externalMediaDirs.length > 0) {
target = externalMediaDirs[0];
}
}
if (target != null) {
strat.addRoot(name, buildPath(target, path)); //将path.xml中定义的path所对应的路径加入到集合
}
}
}
return strat;
}
debug代码也可以看到有如下对应关系
其实,只要提供的路径以path对应路径开头就不会出现Failed to find configured root that contains报错,简单来说<root-path name="root" path="." />是最具通用性,root-path是根目录,其他path满足的话root-path肯定也满足,而且root-path可以兼容外置sd卡。
本文地址:https://blog.csdn.net/u013795543/article/details/109392062