先看下这个异常的官方介绍:FileUriExposedException
The exception that is thrown when an application exposes a file:// Uri to another app.
在Android N(7)以上(API 版本为24),当应用使用file:// 形式的Uri暴露给另一个应用时将会抛出该异常。而低于N之前的版本仍然可以使用file://的形式来共享Uri,但是十分不推荐这样做。
原因在于使用file://Uri会有一些风险,比如:
替代方案是通过FileProvider使用 content:// 形式的Uri并授临时权限给接收该Uri的应用。
下面是一种不太推荐的参考解决方案。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(builder.build());
}
先看下官网的介绍FileProvider
FileProvider是ContentProvider的一个子类,它通过创建content:// 形式的Uri和其他应用之间进行文件安全共享。
Content URI通过使用临时访问权限允许你可以授予读写权限给其它应用,通过Intent.addFlags()添加权限。
作为对比,为了控制 fill:// 形式的Uri的访问权限,你不得不修改底层文件的文件系统权限。这种权限对其他所有应用都可用,直到你改变它。这种访问方式基本上是不安全的。
FileProvider类可以直接拿来使用,不必自己写子类继承它,只需要通过xml来配置。使用它包括以下几个步骤:
要声明FileProvider组件,需要在manifest文件种增加
元素。例如:
...
...
"android.support.v4.content.FileProvider"
android:authorities="com.mydomain.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
...
...
android:name是FileProvider组件的完整类名。
android:authorities是域名,为了保证唯一性,通常是你的应用包名+fileprovider。
android:exported 设置false,因为你不需要暴露它。
android:grantUriPermissions设置true,表示允许你可以对文件授予临时权限。
为了将实际的文件路径(file://)映射成content URI(content://),需要一个配置文件xml来提前定义文件存放的目录路径path与Content URI的对应关系。文件放置在res/xml/下.
"http://schemas.android.com/apk/res/android">
"my_images" path="images/"/>
...
上面的意思是应用私有路径 Context.getFilesDir()的子目录images/ 映射成 content://authorities_name/my_images/ .
File imagePath = new File(Context.getFilesDir(), "images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(),
"com.mydomain.fileprovider", newFile);
例如上面文件default_image.jpg的实际存放路径是file:///data/user/0/com.mydomain.fileprovider/files/images/default_image.jpg
getUriForFile() 返回的content URI为
content://com.mydomain.fileprovider/my_images/default_image.jpg
<files-path name="name" path="path" /> 对应getFilesDir()。
<cache-path name="name" path="path" /> 对应getCacheDir()。
<external-path name="name" path="path" />
对应Environment.getExternalStorageDirectory()。
<external-files-path name="name" path="path" /> 对应getExternalFilesDir()。
<external-cache-path name="name" path="path" /> 对应getExternalCacheDir()。
授予临时权限给getUriForFile()方法返回的content URI,使用下面任一方式即可:
Android设备有很多,SDK API版本有高于或低于24(Android 7)的,有的有SD卡,有的没有。在考虑跨应用访问文件时,例如调用相机拍照、利用DownloadManager下载apk升级等等,如果没有SD卡,那么文件需要存储在内存你的应用空间,那么其他应用可能会因为没有访问权限而产生异常或使程序崩溃。
所以在编程时需要同时考虑设备的版本和有无SD卡。下面就下载升级的apk后进行自动升级分的几种情况:
安装apk通常需要请求系统安装程序来安装(跨应用)写法如下:
public static void installApk(Context context, Uri uri) {
Intent installIntent = new Intent(Intent.ACTION_VIEW);
installIntent.setDataAndType(uri,
"application/vnd.android.package-archive");
installIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//临时授权
context.startActivity(installIntent);
}
如果使用FileProvider,添加的provider_paths.xml示例如下:
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!- 内存卡:context.getCacheDir()-->
<cache-path name="cache" path="."/>
<files-path name="files_download" path="Download" />
<external-files-path name="external_files_download" path="Download" />
paths>
1)使用传统 file:// 形式的Uri
下载的apk文件的Uri为文件存储路径(SD卡):
file:///storage/sdcard/Android/data/com.jykj.departure/files/Download/regular-v1.6.29.apk
apk文件的权限为-rwxrwx—,用户所有者、组都有读写执行的权限,其他程序无权限访问。
linux文件系统种权限10个字符,从左往右第一个 - 表示文件,d表示目录,然后后面每3个一组,分别表示所有者(User)、组(Group)、其他用户(Others)具有的权限。
r表示读(数字代号为“4”);,w表示写(数字2),x表示可执行(数字1)。
一个文件可以被外部程序所读read,该文件的所有上级目录的其他用户组必须具有可执行权限(最后一位为x),表示其他程序可以进入该目录,同时该文件的其他用户组也要为r表示可读。
此时这种形式的Uri是没有问题的。
如果利用DownloadManager下载文件,调用DownloadManger.Query对象的setDestinationInExternalFilesDir()方法可以返回file://形式的Uri 。
思考:此文件最后权限位位空表示其他组是无权限,为什么安装程序可以读此文件?(排除临时授权,本人猜想安装程序对SD卡中的文件拥有的是组权限)
2)如果使用FileProvider将下载的apk文件路径映射成Content URI:
content://com.jykj.departure.fileprovider/external_files_download/regular-v1.6.29.apk
调用installApk()方法将会出现如下异常:
android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.intent.action.VIEW dat=content://com.jykj.departure.fileprovider/external_files_download/regular-v1.6.29.apk typ=application/vnd.android.package-archive flg=0x10000001 }
意思是Android 7以下的版本处理不了这种content URI,找不到组件。
1)使用传统 file:// 形式的Uri
下载的apk文件的Uri为文件存储路径(内存卡):
file:///data/data/com.jykj.departure/cache/regular-v1.6.29.apk
apk文件的权限为-rw——-,表示只有用户所有者具有读和写权限。
此时安装程序虽然可以进入cache目录(最后一位为x),但不能读该文件(无 r )。所以在安装apk时出现的异常是:
There was a problem parsing the package.解析程序包时异常。
在Logcat中打印出的错误信息为:
com.android.packageinstaller W/zipro: Unable to open zip
'/data/data/com.jykj.departure/cache/regular-v1.6.29.apk': Permission denied
怎么办呢?既然没有权限,那就调整文件的权限。
File file =//创建文件
file.setReadable(true,false);//表示全世界都可以读它
可以在文件创建时或下载完成后,调用上面的方法将文件的权限设置其他组用户可读。
这样就不会出问题了。
当然还有另外一种方式,就是通过linux命令,如下:
public static void chmodFile(File file){
String mode = "704";
String[] command = {"chmod", mode, file.getPath() };
ProcessBuilder builder = new ProcessBuilder(command);
try {
builder.start();
} catch (IOException e) {
e.printStackTrace();
}
}
2)如果使用FileProvider将下载的apk文件路径映射成Content URI:
其情况是找不到组件处理该Intent而抛出异常。
如果使用DownloadManager,由于没有SD卡,故无法使用setDestinationInExternalFilesDir()方法来设置存储文件到SD卡路径,所以不必设置,而系统将会默认下载到内存路径/data/user/0/com.android.providers.downloads/cache/app-debug.apk
此时返回的DownloadManager.getUriForDownloadedFile(downid)方法返回的Uri是content:// 形式的Uri,而由于Android7以下版本需要用file://形式的Uri的Intent,所以要使用如下方法获取该文件的file:// 形式的Uri
public static String getFilePathFromUri(Context context, Uri uri) {
String[] proj = {MediaStore.Images.Media.DATA};
Cursor cursor = context.getContentResolver().query(uri, proj, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
int index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
String filePath = cursor.getString(index);
cursor.close();
return filePath;
}
return null;
}
1)使用传统 file:// 形式的Uri
下载的apk文件的Uri为文件存储路径(内存卡):
file:///storage/emulated/0/Android/data/com.jykj.departure/files/Download/regular-v1.6.29.apk
根据Android 7及以上的新特性,这种形式的Uri将会抛出异常:
Caused by: android.os.FileUriExposedException:
file:///storage/emulated/0/Android/data/com.jykj.departure/files/Download/
regular-v1.6.29.apk exposed beyond app through Intent.getData()
2)如果使用FileProvider将下载的apk文件路径映射成Content URI:
content://com.jykj.departure.fileprovider/external_files_download/regular-v1.6.29.apk
此方式则为官方推荐的解决方式。
如果仅仅使用Content URI,但没有授读权限,也会出现异常:
java.lang.SecurityException: Permission Denial:
opening provider android.support.v4.content.FileProvider from ProcessRecord
所以installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//临时授权
这个不能少!
如果使用DownloadManager并设置文件存在在SD卡,那么需要FileProvider来映射该路径,目的是使用content:// 形式的Uri。
1)使用传统 file:// 形式的Uri
下载的apk文件的Uri为文件存储路径(内存卡):
file:///data/user/0/com.jykj.departure/cache/regular-v1.6.29.apk
很明显根据Android 7及以上的新特性,这种形式的Uri同样将会抛出异常:
Caused by: android.os.FileUriExposedException:
file:///storage/emulated/0/Android/data/com.jykj.departure/files/Download/
regular-v1.6.29.apk exposed beyond app through Intent.getData()
即使更改文件权限也解决不了问题了!
2)如果使用FileProvider将下载的apk文件路径映射成Content URI:
虽然文件存储路径为(内存卡):
file::///data/user/0/com.jykj.departure/cache/regular-v1.6.29.apk
但是通过映射成Content URI并临时授权(读):
content://com.jykj.departure.fileprovider/cache/regular-v1.6.29.apk
使得安装程序还是可以读文件。此方式则为官方推荐的解决方式。
如果使用DownloadManager,由于无SD卡,所以不必设置路径,默认会下载内存中,返回的Uri也是content:// 形式的,此时可以直接使用。
在使用DownloadManager下载文件时,如果设备无SD卡,存储路径就无法设置成SD卡路径,所以不能设置,而系统会默认下载到内存路径/data/user/0/com.android.providers.downloads/cache/app-debug.apk,此时通过DownloadManager.getUriForDownloadedFile(downid)方法得到的是content:// 形式的Uri,这在Android 7及以上是可以识别的Uri并能够执行安装,但在7以下是无法识别的,会抛出android.content.ActivityNotFoundException: No Activity found to handle Intent 的运行时异常。
当然有SD卡时也是可以使用DownloadManager的,但Android 7及以上需要使用FileProvider将file:// 路径转换成content:// 形式Uri来传递Intent,7以下则使用file:// 形式的Uri来传递Intent.
所以要利用DownloadManager的方便性,需要考虑无SD卡的情况。
所以通过上述分析,以上四种情况都需要考虑,在代码中都需要作分支处理:
1)分情况获取文件的Uri
//是否有外存
public static boolean hasExternalStorage() {
return Environment.getExternalStorageState().
equals(Environment.MEDIA_MOUNTED);
}
//根据文件获取Uri
public static Uri getUriForFile(Context context, File file) {
Log.e("WS","SD卡:"+hasExternalStorage()+",downloadApk path:"+file);
Uri fileUri = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {//24 android7
fileUri = FileProvider.getUriForFile(context,FILE_PROVIDER_AUTH, file);
} else {
if(!ApplicationHelper.hasExternalStorage())
ApplicationHelper.chmodFile(file);//没有SD卡,需要更改文件权限
//file.setReadable(true,false);
fileUri = Uri.fromFile(file);
}
Log.e("WS","downloadApk Uri:"+fileUri);
return fileUri;
}
2)安装apk:
public static void installApk(Context context, Uri uri) {
Intent installIntent = new Intent(Intent.ACTION_VIEW);
installIntent.setDataAndType(uri,
"application/vnd.android.package-archive");
installIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//临时授权
context.startActivity(installIntent);
}
3)Http下载apk
public static File downloadFile(String url, File saveDir) throws IOException {
// 获得连接
HttpURLConnection conn = (HttpURLConnection) new URL(url)
.openConnection();
// 设置超时时间为6000毫秒,conn.setConnectionTiem(0);表示没有时间限制
conn.setConnectTimeout(6000);
// 连接设置获得数据流
conn.setDoInput(true);
// 不使用缓存
conn.setUseCaches(false);
// 这句可有可无,没有影响
conn.connect();
// 得到数据流
InputStream is = conn.getInputStream();
if(!saveDir.exists()){
saveDir.mkdirs();
}
File file = new File(saveDir,ApplicationHelper.getNetworkFileName(url));
OutputStream os = new FileOutputStream(file);
byte[] buff = new byte[4*1024];
int len = -1;
while ((len=is.read(buff))!=-1){
os.write(buff,0,len);
}
os.flush();
os.close();
is.close();
return file;
}
4)DownloadManager下载文件
//使用此下载方法需要考虑有无SD卡,是否Android 7版本
public static long download(Context context,String url) {
DownloadManager manager = (DownloadManager) context
.getSystemService(Context.DOWNLOAD_SERVICE); // 初始化下载管理器
Request request = new Request(Uri.parse(url));// 创建请求
//request.setAllowedNetworkTypes(Request.NETWORK_MOBILE | Request.NETWORK_WIFI);// 设置允许使用的网络类型,这里是移动网络和wifi都可以
request.setNotificationVisibility(Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
//request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
request.setAllowedOverRoaming(false);// 漫游
//判断是否有SD卡,如果有设置路径,没有则使用默认内存路径
//默认路径:/data/user/0/com.android.providers.downloads/cache/app-debug.apk
if(ApplicationHelper.hasExternalStorage())
request.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, ApplicationHelper.getNetworkFileName(url));
return manager.enqueue(request);// 将下载请求放入队列
}
//DownloadManager
private static void dmDownload(Context mContext, String apkURL){
Uri uri = checkDownloadManagerFileExist(mContext, apkURL);
uri = getUriForUri(mContext,uri);
if (uri != null){
ApplicationHelper.installApk(mContext, uri);
}
else HttpHelper.download(mContext, apkURL);
}
//检查DownloadManager是否已下载过 文件
//fileUrl : apk下载地址如:http://xxxx.apk
public static Uri checkDownloadManagerFileExist(Context context, String fileUrl) {
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
if (downloadManager == null) return null;
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL);// 设置过滤状态:成功
Cursor c = downloadManager.query(query);// 查询以前下载过的‘成功文件’
Log.e("KALY", "DownloadManager 文件数量:" + c.getCount());
while (c.moveToNext()) {
if (c.getString(c.getColumnIndex(DownloadManager.COLUMN_URI)).equals(fileUrl)) {
Uri uri = Uri.parse(c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)));
Log.e("KALY","文件Uri:"+uri);
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.N||ApplicationHelper.hasExternalStorage()||
ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())){
c.close();
return uri;
}
}
}
c.close();
return null;
}
//DownloadManager 下载 分4种情况讨论
public static Uri getUriForUri(Context context,Uri uri){
Log.e("KALY","原始uri:"+uri);
if(uri ==null) return null;
//判断文件是否存在
String path=ContentResolver.SCHEME_FILE.equals(uri.getScheme())?
uri.getPath():getFilePathFromUri(context, uri);
Log.e("KALY","path:"+path);
if(path==null) return null;
File file =new File(path);
Log.e("KALY","file exists:"+file.exists());
//android 7以下 需要使用 file:// URI
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
if(!file.exists()) return null;
if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
uri =Uri.fromFile(file);
}
}
//android 7 以上,需要使用 content:// URI
else {
if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
if(!file.exists()) return null;
uri =FileProvider.getUriForFile(context, FILE_PROVIDER_AUTH,file);
}
}
Log.e("KALY","转化后的uri:"+uri);
return uri;
}
FileProvider配置和相应的xml文件此处省略。
《道德经》第二十七章:
善行,无辙迹;善言,无瑕谪;善数,不用筹策;善闭,无关楗而不可开;善结,无绳约而不可解。是以圣人常善救人,故无弃人;常善救物,故无弃物。是谓袭明。故善人者,不善人之师;不善人者,善人之资。不贵其师,不爱其资,虽智大迷,是谓要妙⑩。
译文:善于行走的,不会留下辙迹;善于言谈的,不会发生病疵;善于计数的,用不着竹码子;善于关闭的,不用栓梢而使人不能打开;善于捆缚的,不用绳索而使人不能解开。因此,圣人经常挽救人,所以没有被遗弃的人;经常善于物尽其用,所以没有被废弃的物品。这就叫做内藏着的聪明智慧。所以善人可以做为恶人们的老师,不善人可以作为善人的借鉴。不尊重自己的老师,不爱惜他的借鉴作用,虽然自以为聪明,其实是大大的糊涂。这就是精深微妙的道理。