因工作原因偎球,學(xué)習(xí)了FileProvider
憎兽。本文主要摘要關(guān)鍵知識點(diǎn)和記錄我的學(xué)習(xí)思路及驗(yàn)證結(jié)論议街,可以幫助讀者比較全面的認(rèn)識FileProvider
汽久。如讀者尚未了解何為FileProvider
肛著,請閱讀安卓官網(wǎng)的FileProvider
參考和分享文件指南甲喝。
目錄
- FileProvider的基本面
- 最小原型
- 源應(yīng)用各項(xiàng)配置的說明
- 怎么實(shí)現(xiàn)端對端的uri傳遞
- FileProvider的展開
- 權(quán)限管理
- 多個FileProvider并存
- 自定義Uri格式
- FileProvider的深入
FileProvider的基本面
最小原型
FileProvider
是特殊的ContentProvider
悠咱,目標(biāo)是在為保護(hù)隱私和數(shù)據(jù)安全而加強(qiáng)應(yīng)用沙箱機(jī)制的同時荣瑟,支持在應(yīng)用間共享文件。關(guān)于ContentProvider
的方方面面足绅,請參考安卓官網(wǎng)的相關(guān)參考和指南捷绑。
下圖是FileProvider
的工作模型:
下面假設(shè)存在源應(yīng)用沙箱的files/some/internal/path/1.dat
文件共享給目標(biāo)應(yīng)用,展示雙方應(yīng)用要達(dá)成目標(biāo)的最小代碼原型编检。首先是源應(yīng)用:
// build.gradle
dependencies {
implementation 'androidx.appcompat:appcompat:+'
}
<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.provider">
<provider android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/paths"/>
</provider>
</manifest>
<!-- res/xml/paths.xml -->
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="name-example" path="some/internal/path" />
<!-- write extra paths rules here -->
</paths>
然后是目標(biāo)應(yīng)用:
Uri uri = Uri.parse("content://com.example.provider.fileprovider/name-example/1.dat");
InputStream istream = getContentResolver().openInputStream(uri);
// ParcelFileDesciptor fd = getContentResolver().openFileDescriptor(uri, ...);
// read from istream or operate fd
// ...
以上就是讓FileProvider
能夠成功運(yùn)行的核心代碼(最小原型)胎食。如果要在正式的工程項(xiàng)目中使用FileProvider
扰才,還需要一些額外代碼允懂,但始終都不脫離上述核心代碼。下面對一些基礎(chǔ)要點(diǎn)展開介紹衩匣。
源應(yīng)用各項(xiàng)配置的說明
android:name
如上文所說蕾总,FileProvider
是ContentProvider
的子類,AndroidManifest.xml
的配置標(biāo)簽也是<provider/>
琅捏,所以FileProvider
也屬于四大組件生百。跟所有四大組件一樣,android:name
就是FileProvider
的實(shí)現(xiàn)者類名柄延。
FileProvider
的name
默認(rèn)指定androidx.core.content.FileProvider
就夠了蚀浆,但這并不是嚴(yán)格要求。某些應(yīng)用場景會需要提供androidx.core.content.FileProvider
的子類搜吧,關(guān)于這個話題將在后面的章節(jié)展開介紹市俊。
androidx.core.content.FileProvider
是androidx.core:core:+
提供的,可以直接添加androidx.core:core:+
依賴滤奈、或通過androidx.appcompat:appcompat:+
間接依賴摆昧。
android:authorities
參考<provider>的指南,FileProvider
沒有特殊要求蜒程。
android:export
FileProvider
要求本字段必須配置false
绅你,然后針對uri
授予臨時權(quán)限。配置true
會導(dǎo)致編譯期報(bào)錯昭躺。本字段的更多說明請參考<provider>的指南忌锯。關(guān)于權(quán)限的問題,參考權(quán)限管理一節(jié)领炫。
<paths/>
本配置是FileProvider
提供的安全策略偶垮,可以隱藏沙箱目錄的一些具體細(xì)節(jié)。文件必須位于<paths/>
標(biāo)簽下配置的目錄下,才可以被FileProvider
共享针史。
<paths/>
標(biāo)簽下可以插入多條配置晶伦。對files
目錄下的文件需要用<files-path/>
標(biāo)簽配置策略,如上文的示例代碼啄枕。<paths/>
標(biāo)簽下還支持配置緩存目錄婚陪、外存目錄、等其他目錄频祝,詳細(xì)說明請參考FileProvider參考泌参。
<paths/>
的配置會影響文件的uri
,如上文示例代碼那樣常空。詳細(xì)說明參考后續(xù)章節(jié)uri的默認(rèn)規(guī)則沽一。
怎么實(shí)現(xiàn)端對端的uri傳遞
ContentProvider
的uri
通常由源應(yīng)用定義。除非源應(yīng)用和目標(biāo)應(yīng)用有過事先約定漓糙,否則目標(biāo)應(yīng)用是很難自己生成正確uri
的铣缠。FileProvider
封裝的PathStrategy
,并基于PathStrategy
提供了一套生成uri
的規(guī)則昆禽。
uri的默認(rèn)規(guī)則
在源應(yīng)用中蝗蛙,uri
需要通過FileProvider.getUriFromFile(..., file)
獲取,方法內(nèi)部會遍歷PathStrategy
的所有策略醉鳖,根據(jù)匹配的策略把文件路徑映射為uri
捡硅。相對的,在目標(biāo)應(yīng)用調(diào)用FileProvider
讀寫文件的時候盗棵,FileProvider
會根據(jù)相同的PathStrategy
反向把uri
映射為文件路徑壮韭。
在上文的示例代碼中,文件路徑files/some/internal/path/1.dat
命中了規(guī)則<files-path name="name-example" path="some/internal/path" />
纹因,其中files
對應(yīng)<files-path/>
喷屋、some/internal/path
對應(yīng)path="..."
。FileProvider
會把files/some/internal/path
部分替換為name="..."
的值辐怕,加上FileProvider
的authority
逼蒙,就得到了content://com.example.provider.fileprovider/name-example/1.dat
。
類似的寄疏,如果上文示例代碼存在如下配置:
<!-- res/xml/paths.xml -->
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="name-example" path="some/internal/path" />
<files-path name="another-example" path="another/internal/path" />
</paths>
假設(shè)要共享的文件為files/another/internal/path/some/image/2.png
是牢,則映射uri
的結(jié)果是content://com.example.provider.fileprovider/another-example/some/image/2.png
。
基于FileProvider
的映射規(guī)則陕截,只要①FileProvider
事先完成了對uri
的授權(quán)驳棱,且②目標(biāo)應(yīng)用預(yù)先知道了某個文件的相對路徑,那么從技術(shù)上來說农曲,目標(biāo)應(yīng)用可以不需要源應(yīng)用告訴社搅,就能自己根據(jù)源應(yīng)用的<paths/>
配置生成正確的uri
驻债。在實(shí)際項(xiàng)目中仍然需要應(yīng)用間通過IPC途徑傳遞uri
,正是因?yàn)樯鲜觫佗趦牲c(diǎn)很難滿足形葬、且不應(yīng)輕易滿足合呐。
通過Intent傳遞uri
Intent
是常用的進(jìn)程間通信載體。通過Intent
傳遞uri
的最小原型如下:
Uri uri = ...;
Intent intent = new Intent();
intent.setData(uri);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// send the intent
-
uri
一定要通過setData
(在APILEVEL ∈ [16, 22]
的設(shè)備上需要使用setClipData()方法)設(shè)置笙以; - 一定要通過
setFlags
設(shè)置uri
的讀寫權(quán)限淌实;
如果上述兩點(diǎn)沒有滿足,目標(biāo)應(yīng)用在使用uri
的時候會得到一個java.lang.SecurityException: Permission Denial
異常猖腕。
上面的Intent
可以通過多種方式發(fā)送到目標(biāo)應(yīng)用:
-
Context.startActivity(intent)
:如調(diào)用另一個應(yīng)用打開沙箱內(nèi)的一個文檔拆祈; -
Activity.setResult(intent)
:如調(diào)用一個文件選擇器返回一個文檔; -
Context.startService(intent)
倘感; - Android定義的其他其他
Intent
發(fā)送的手段放坏;
上述方案除了uri
授權(quán)的有效期略有不同以外,本質(zhì)上是一樣的老玛,可依據(jù)具體應(yīng)用場景選用淤年。關(guān)于uri
授權(quán)有效期的問題,會在權(quán)限管理一節(jié)介紹逻炊。
通過Intent以外的IPC方式傳遞uri
典型的方法是Binder互亮。例如定義如下aidl:
interface IDocumentRepositoty {
Uri requestDocument(String myPackageName, String documentName);
}
關(guān)于Binder和aidl的使用方法,可參考Android 接口定義語言 (AIDL)余素,本文不做展開。
在源應(yīng)用返回uri
之前炊昆,一定要通過Context.grantUriPermissions()
方法設(shè)置uri
的讀寫權(quán)限桨吊,否則目標(biāo)應(yīng)用在使用uri
時會得到一個java.lang.SecurityException: Permission Denial
異常。
public class RepositoryImpl implements IDocumentRepositoty.Stub {
Context context = ...;
public Uri requestDocument(String toPackageName, String documentName) {
Uri uri = ...;
context.grantUriPermissions(toPackageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
return uri;
}
}
權(quán)限管理一節(jié)會對Context.grantUriPermissions()
做更多介紹凤巨。
FileProvider的展開
權(quán)限管理
基本點(diǎn)
權(quán)限管理的目標(biāo)是控制所有uri
的讀寫權(quán)限视乐,權(quán)限可以是只讀、只寫敢茁、可讀可寫佑淀。所有uri
在通過授權(quán)之前,默認(rèn)是不能被讀寫的彰檬,否則會收到java.lang.SecurityException: Permission Denial
異常伸刃。對只讀uri
做寫操作、或?qū)χ粚?code>uri做讀操作逢倍,都會收到異常捧颅。
授權(quán)的粒度是uri×目標(biāo)應(yīng)用包名
。對相同目標(biāo)應(yīng)用较雕,不同的uri
要分別授權(quán)碉哑;對相同的uri
、不同的目標(biāo)應(yīng)用也要分別授權(quán)】鄣洌基于這樣的粒度妆毕,所以不用擔(dān)心預(yù)期之外的應(yīng)用強(qiáng)行讀寫uri
,也不用擔(dān)心授權(quán)的目標(biāo)應(yīng)用隨意生成uri
枚舉源應(yīng)用內(nèi)的文件贮尖。
在通過Intent
傳遞uri
的時候设塔,如果通過Intent.setFlags()
設(shè)置了讀或?qū)憴?quán)限,那么有且只有收到Intent
的應(yīng)用能獲得授權(quán)远舅。收到Intent
后闰蛔,該應(yīng)用的所有代碼都能獲得授權(quán),跟收到Intent
的是Activity
图柏、Service
序六、或其他組件無關(guān)。
如果沒有通過Intent.setFlags()
授權(quán)蚤吹,則需要通過Context.grantUriPermissions(toPackage, uri, flags)
授權(quán)例诀,其中參數(shù)toPackage
是目標(biāo)應(yīng)用的包名。
授權(quán)的有效期
uri
的授權(quán)都是臨時授權(quán)裁着。根據(jù)授權(quán)方式不同繁涂,授權(quán)的有效期和過期規(guī)則略有差異。一旦授權(quán)過期或取消了二驰,就需要源應(yīng)用重新授權(quán)扔罪。
通過Intent.setFlags()
授權(quán),根據(jù)接收Intent
的組件不同桶雀,授權(quán)有效期的判斷依據(jù)有差異:
- 如果
Intent
接收組件為Activity
矿酵,則其所在棧的所有Activity
執(zhí)行onDestroy
之后,授權(quán)就過期了矗积; - 如果
Intent
的接收組件為Service
全肮,則該Service
執(zhí)行onDestroy
之后,授權(quán)就過期了棘捣;
通過Context.grantUriPermissions(toPackage, ...)
授權(quán)辜腺,當(dāng)toPackage
指向的應(yīng)用的所有進(jìn)程都結(jié)束后,授權(quán)就過期了乍恐。
除了上述由Android管理的過期策略评疗,應(yīng)用還可以調(diào)用Context.revokeUriPermission(uri, ...)
主動收回授權(quán)。
限制可共享文件的范圍
通過FileProvider
共享的文件禁熏,都必須位于<paths/>
配置包含的目錄下壤巷;分享一個不在這些目錄下的文件會在調(diào)用getUriFromFile
的時候收到一個異常。為敘述方便瞧毙,下文將這些符合<paths/>
配置的文件簡稱為“paths集合”胧华。
在上文最小原型示例中寄症,FileProvider
的android:grantUriPermissions
字段配置為true
,其效果是所有屬于paths集合的文件都可以共享矩动。如果android:grantUriPermissions
配置為false
有巧,則需要配置<grant-uri-permission/>
定義一個子集(下文簡稱為“grant集合”)。paths集合和grant集合的交集才是可以共享的文件集合悲没。更多說明請參考官網(wǎng)指南:android:grantUriPermissions和<grant-uri-permission/>篮迎。
多個FileProvider并存
Android允許定義多個FileProvider
,應(yīng)用構(gòu)建的時候AGP似乎并不會校驗(yàn)這些FileProvider
配置是否有重復(fù)或沖突示姿,但是在運(yùn)行時可能會得到預(yù)期之外的結(jié)果甜橱。
這里列出一些典型的情況(假設(shè)配置了兩個FileProvider
,且兩者的<paths/>
配置不同):
- 如果
android:name
相同栈戳、android:authorities
也相同:只有寫在前面的FileProvider
是有效的岂傲,后面的FileProvider
的<paths/>
配置對getUriForFile()
不可見;源應(yīng)用調(diào)用getUriForFile()
獲取第二個FileProvider
的uri
的時候子檀,會得到java.lang.IllegalArgumentException: Failed to find configured root
異常镊掖。 - 如果
android:name
相同、android:authorities
不同:源應(yīng)用在調(diào)用getUriForFile()
的時候能得到正確的uri
褂痰;目標(biāo)應(yīng)用通過uri
訪問文件的時候亩进,只能解析寫在前面的FileProvider
的uri
,解析后面的FileProvider
的uri
時會得到java.lang.SecurityException: The authority does not match
異常缩歪。 - 如果
android:name
不同(如繼承自FileProvider
的子類)归薛、android:authorities
相同:源應(yīng)用會得到跟第一種情況相同的結(jié)果。 - 如果
android:name
不同驶冒、android:authorities
也不同:源應(yīng)用調(diào)用getUriForFile()
時傳入正確的authority
就能得到正確的uri
苟翻;目標(biāo)應(yīng)用也可以成功的訪問uri
指向的文件。
基于上面的情況骗污,項(xiàng)目中每個模塊在提供FileProvider
的時候,比較好的做法是:
-
android:name
用從FileProvider
繼承的子類類名沈条; -
android:authorities
使用不容易跟別人重復(fù)的值需忿;
自定義Uri格式
FileProvider
可以被繼承,Android允許子類重載FileProvider
的默認(rèn)行為蜡歹。這里介紹如何通過重載FileProvider
來自定義uri
格式屋厘。
下面演示如何把形如content://${authority}/${name}/${relativePath}
的uri
按照content://${authority}/${md5FromFilePath}
的格式加密,如content://com.example.fileprovider/c2681e80365f7f9f041875cbd25e4c20
月而。如果源應(yīng)用想對目標(biāo)應(yīng)用完全隱藏其文件在沙箱中的路徑信息汗洒,可以考慮類似方案。
首先繼承FileProvider
并重載所有openFile()
:
public class MyFileProvider extends FileProvider {
static Map<Uri, Uri> mappedUris = new ConcurrentHashMap<>(); // alternative to original
public static Uri getUriForFile(@NonNull Context context, @NonNull String authority, @NonNull File file) {
Uri original = FileProvider.getUriForFile(context, authority, file);
String md5 = getMD5(original.getPath());
Uri alternative = Uri.parse(original.getScheme() + "://" + original.getAuthority() + "/" + md5);
synchronized (mappedUris) {
for (Entry<Uri, Uri> entry : mappedUris.entrySet()) {
if (entry.getValue().equals(original)) {
return entry.getKey();
}
}
mappedUris.put(alternative, original);
}
return alternative;
}
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
Uri originalUri = mappedUris.get(uri);
if (originalUri == null) {
throw new FileNotFoundException();
}
return super.openFile(originalUri, mode);
}
//...
}
然后使用加密后的uri
:
// Uri normalUri = FileProvider.getUriForFile(context, authority, sourceFile);
Uri hashedUri = MyFileProvider.getUriForFile(context, authority, sourceFile);
intent.setData(hashedUri); // 因?yàn)橹剌d了openFile()父款,所以傳遞normalUri會讓目標(biāo)應(yīng)用收到一個FileNotFoundException
目標(biāo)應(yīng)用不需要做任何修改溢谤。