本篇文章已授權(quán)為微信公眾號 code小生 發(fā)布
一、前言
對于 Android 7.0,提供了非常多的變化,不過和我們開發(fā)者關(guān)聯(lián)最大的,或者說必須要適配的就是去除項(xiàng)目中傳遞 file:// 類似格式的 Uri 了。
對于面向 Android 7.0 的應(yīng)用,Android 框架執(zhí)行的 StrictMode API 政策禁止在應(yīng)用外部公開 file:// URI , 如果一項(xiàng)包含文件 URI 的 intent 離開應(yīng)用,則應(yīng)用出現(xiàn)故障,并出現(xiàn) FileUriExposedException 異常。
要應(yīng)用間共享文件,您應(yīng)發(fā)送一項(xiàng) content:// URI,并授予 URI 臨時(shí)訪問權(quán)限。進(jìn)行此授權(quán)的最簡單方式是使用 FileProvider 類。如需了解有關(guān)權(quán)限和共享文件的詳細(xì)信息,請參閱 共享文件
FileProvider 實(shí)際上是 ContentProvider 的一個子類,它的作用也比較明顯,file://Uri 不給用,那么換個 Uri 為 content:// 來替代。
二、Provider 使用詳解
1、定義 FileProvider
我們先在 AndroidManifest 中進(jìn)行注冊
<manifest>
...
<application>
...
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.developerhaoz.androidtrainingdemo.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
...
</provider>
...
</application>
</manifest>
為什么要申明呢?當(dāng)然是因?yàn)?FileProvider 是 ContentProvider 的子類啊。
2、指定可分享的文件路徑
FileProvider 只能為指定的目錄中的文件生成內(nèi)容 URI。要指定目錄,就必須使用 <paths> 元素的子元素在 XML 中指定其存儲區(qū)域和路徑。
我們先創(chuàng)建一個名為 res/xml/filepaths.xml 的新文件

在 filepaths.xml 文件中,便可以指定文件存儲的區(qū)域和路徑。例如,以下路徑元素告訴 FileProvider,你打算為私有文件區(qū)域的 images/ 子目錄 請求內(nèi)容 URI
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="my_images" path="images/"/>
...
</paths>
<paths> 必須包含以下元素中一個或者多個子元素
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<root-path name="root" path="" />
<files-path name="files" path="" />
<cache-path name="cache" path="" />
<external-path name="external" path="" />
<external-files-path name="name" path="path" />
<external-cache-path name="name" path="path" />
</paths>
在 paths 節(jié)點(diǎn)內(nèi)部支持以下幾個子節(jié)點(diǎn),分別為:
| 子節(jié)點(diǎn) | 含義 |
|---|---|
| <root-path> | 代表設(shè)備的根目錄 new File("/") |
| <files-path> | 代表 context.getFileDir() |
| <cache-path> | 代表 context.getCacheDir() |
| <external-path> | 代表 Environment.getExternalStorageDirectory() |
| <external-files-path> | 代表 context.getExternalFilesDirs() |
| <external-cache-path> | 代表 getExternalCacheDirs() |
每個節(jié)點(diǎn)都使用兩個屬性:
- name
- path
path 即為代表目錄下的子目錄,例如:
<external-path name="external" path="pics"/>
代表的目錄即為:Environment.getExternalStorageDirectory()/pics
當(dāng)這么聲明以后,代碼可以使用你所聲明的當(dāng)前文件夾以及其子文件夾
可能你會有疑問,為什么要寫這么個 xml 文件,有啥用呢?
我們剛才說了,現(xiàn)在要使用 content://uri 替換 file://uri,那么,content:// 的 uri 如何定義呢?總不能使用文件路徑吧,那不是騙自己么
所以,需要一個虛擬的路徑對文件路徑進(jìn)行映射,所以需要編寫個 xml 文件,通過 path 以及 xml 節(jié)點(diǎn)確定可訪問的目錄,通過 name 屬性來映射真實(shí)的文件路徑
寫好 filepaths.xml 文件之后,要將此文件鏈接到 FileProvider 中,就必須添加一個 <meta-data> 元素作為定義 FileProvider 的 <provider> 元素的子元素。將 <meta-data> 元素的 android : name 屬性設(shè)置為 android.support.FILE_PROVIDER_PATHS, 將元素的 "android : resource" 屬性設(shè)置為 @xml / filepaths (注意不要指定 .xml 拓展名)。例如:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.developerhaoz.androidtrainingdemo.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths"
/>
</provider>
3、使用 FileProvider 生成內(nèi)容 URI
配置工作已經(jīng)全部完成了,后面就需要將之前傳遞的 file:// 替換成 FileProvoider 需要用到的 content://,這就需要用到 FileProvider.getUriForFile() 方法了
public static Uri getUriForFile(Context context, String authority, File file) {
final PathStrategy strategy = getPathStrategy(context, authority);
return strategy.getUriForFile(file);
}
可以看到 getUriForFile(),需要傳入 一個
authority 的參數(shù),這正是我們前面在 AndroidManifest.xml 文件中配置的 android:authorities 參數(shù)
調(diào)用這個方法會自動得到一個 file:// 轉(zhuǎn)換成 content:// 的一個 Uri 對象,可以供我們直接使用
4、給 Uri 授予臨時(shí)權(quán)限
當(dāng)我們生成一個 content:// 的 Uri 對象之后,其實(shí)還無法對其直接使用,還需要對這個 Uri 接收的 App 賦予對應(yīng)的權(quán)限才可以。
這個授權(quán)的動作,提供了兩種方式來授權(quán):
① 通過 Context 的 grantUriPermission() 方法授權(quán)
Context 提供了兩個方法
- grantUriPermission(String toPackage, Uri uri, int modeFlags)
- revokeUriPermission(Uri uri, int modeFlags);
可以看到 grantUriPermission() 方法需要傳遞一個包名,就是你給哪個應(yīng)用授權(quán),但是很多時(shí)候,比如分享,我們并不知道最終用戶會選擇哪個 app,所以我們可以這樣:
List<ResolveInfo> resInfoList = context.getPackageManager()
.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
context.grantUriPermission(packageName, uri, flag);
}
根據(jù) Intent 查詢出所有符合的應(yīng)用,都給他們授權(quán),然后在不需要的時(shí)候通過 revokeUriPermission 移除權(quán)限。
② 配合 Intent.addFlags() 授權(quán)
既然這是一個 Intent 的 Flag,Intent 也提供了另外一種比較方便的授權(quán)方式,那就是使用 Intent.setFlags() 或者 Intent.addFlag 的方式
使用這種形式的授權(quán),權(quán)限截止于該 App 所處的堆棧被銷毀。也就是說,一旦授權(quán),直到該 App 被完全退出,這段時(shí)間內(nèi),該 App 享有對此 Uri 指向的文件的對應(yīng)權(quán)限,我們無法主動收回該權(quán)限了。
三、總結(jié)
Android 7.0 禁止在應(yīng)用外部公開 file:// URI,所以我們必須使用 content:// 替代 file://,這時(shí)主要需要 FileProvider 的支持,而因?yàn)?FileProvider 是 ContentProvider 的子類,所以需要在 AndroidManifest.xml 文件中進(jìn)行注冊,而又因?yàn)樾枰獙φ鎸?shí)的 filepath 進(jìn)行映射,所以需要編寫一個 xml 文檔,用于描述可使用的文件夾目錄,以及通過 name 去映射該文件夾目錄。
當(dāng)我們生成一個 content:// 的 Uri 對象之后,還需要對這個 Uri 接收的 App 賦予對應(yīng)的權(quán)限,到此本文的內(nèi)容就基本結(jié)束了。