FileProvider學(xué)習(xí)筆記之一

因工作原因偎球,學(xué)習(xí)了FileProvider憎兽。本文主要摘要關(guān)鍵知識點(diǎn)和記錄我的學(xué)習(xí)思路及驗(yàn)證結(jié)論议街,可以幫助讀者比較全面的認(rèn)識FileProvider汽久。如讀者尚未了解何為FileProvider肛著,請閱讀安卓官網(wǎng)的FileProvider參考分享文件指南甲喝。

目錄

  1. FileProvider的基本面
    • 最小原型
    • 源應(yīng)用各項(xiàng)配置的說明
    • 怎么實(shí)現(xiàn)端對端的uri傳遞
  2. FileProvider的展開
    • 權(quán)限管理
    • 多個FileProvider并存
    • 自定義Uri格式
  3. FileProvider的深入

FileProvider的基本面

最小原型

FileProvider是特殊的ContentProvider悠咱,目標(biāo)是在為保護(hù)隱私和數(shù)據(jù)安全而加強(qiáng)應(yīng)用沙箱機(jī)制的同時荣瑟,支持在應(yīng)用間共享文件。關(guān)于ContentProvider的方方面面足绅,請參考安卓官網(wǎng)的相關(guān)參考指南捷绑。

下圖是FileProvider的工作模型:

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

如上文所說蕾总,FileProviderContentProvider的子類,AndroidManifest.xml的配置標(biāo)簽也是<provider/>琅捏,所以FileProvider也屬于四大組件生百。跟所有四大組件一樣,android:name就是FileProvider的實(shí)現(xiàn)者類名柄延。

FileProvidername默認(rèn)指定androidx.core.content.FileProvider就夠了蚀浆,但這并不是嚴(yán)格要求。某些應(yīng)用場景會需要提供androidx.core.content.FileProvider的子類搜吧,關(guān)于這個話題將在后面的章節(jié)展開介紹市俊。

androidx.core.content.FileProviderandroidx.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傳遞

ContentProvideruri通常由源應(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="..."的值辐怕,加上FileProviderauthority逼蒙,就得到了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
  1. uri一定要通過setData(在APILEVEL ∈ [16, 22]的設(shè)備上需要使用setClipData()方法)設(shè)置笙以;
  2. 一定要通過setFlags設(shè)置uri的讀寫權(quán)限淌实;

如果上述兩點(diǎn)沒有滿足,目標(biāo)應(yīng)用在使用uri的時候會得到一個java.lang.SecurityException: Permission Denial異常猖腕。

上面的Intent可以通過多種方式發(fā)送到目標(biāo)應(yīng)用:

  1. Context.startActivity(intent):如調(diào)用另一個應(yīng)用打開沙箱內(nèi)的一個文檔拆祈;
  2. Activity.setResult(intent):如調(diào)用一個文件選擇器返回一個文檔;
  3. Context.startService(intent)倘感;
  4. 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集合”胧华。

在上文最小原型示例中寄症,FileProviderandroid: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/>配置不同):

  1. 如果android:name相同栈戳、android:authorities也相同:只有寫在前面的FileProvider是有效的岂傲,后面的FileProvider<paths/>配置對getUriForFile()不可見;源應(yīng)用調(diào)用getUriForFile()獲取第二個FileProvideruri的時候子檀,會得到java.lang.IllegalArgumentException: Failed to find configured root異常镊掖。
  2. 如果android:name相同、android:authorities不同:源應(yīng)用在調(diào)用getUriForFile()的時候能得到正確的uri褂痰;目標(biāo)應(yīng)用通過uri訪問文件的時候亩进,只能解析寫在前面的FileProvideruri,解析后面的FileProvideruri時會得到java.lang.SecurityException: The authority does not match異常缩歪。
  3. 如果android:name不同(如繼承自FileProvider的子類)归薛、android:authorities相同:源應(yīng)用會得到跟第一種情況相同的結(jié)果。
  4. 如果android:name不同驶冒、android:authorities也不同:源應(yīng)用調(diào)用getUriForFile()時傳入正確的authority就能得到正確的uri苟翻;目標(biāo)應(yīng)用也可以成功的訪問uri指向的文件。

基于上面的情況骗污,項(xiàng)目中每個模塊在提供FileProvider的時候,比較好的做法是:

  1. android:name用從FileProvider繼承的子類類名沈条;
  2. 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)用不需要做任何修改溢谤。

FileProvider的底層原理

未完待續(xù)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瞻凤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子世杀,更是在濱河造成了極大的恐慌阀参,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瞻坝,死亡現(xiàn)場離奇詭異蛛壳,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)所刀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門衙荐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人浮创,你說我怎么就攤上這事忧吟。” “怎么了蒸矛?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵瀑罗,是天一觀的道長。 經(jīng)常有香客問我雏掠,道長斩祭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任乡话,我火速辦了婚禮摧玫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘绑青。我一直安慰自己诬像,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布闸婴。 她就那樣靜靜地躺著坏挠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪邪乍。 梳的紋絲不亂的頭發(fā)上降狠,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機(jī)與錄音庇楞,去河邊找鬼榜配。 笑死,一個胖子當(dāng)著我的面吹牛吕晌,可吹牛的內(nèi)容都是我干的蛋褥。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼睛驳,長吁一口氣:“原來是場噩夢啊……” “哼烙心!你這毒婦竟也來了膜廊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤弃理,失蹤者是張志新(化名)和其女友劉穎溃论,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體痘昌,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钥勋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了辆苔。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片算灸。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖驻啤,靈堂內(nèi)的尸體忽然破棺而出菲驴,到底是詐尸還是另有隱情,我是刑警寧澤骑冗,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布赊瞬,位于F島的核電站,受9級特大地震影響贼涩,放射性物質(zhì)發(fā)生泄漏巧涧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一遥倦、第九天 我趴在偏房一處隱蔽的房頂上張望谤绳。 院中可真熱鬧,春花似錦袒哥、人聲如沸缩筛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瞎抛。三九已至,卻和暖如春却紧,著一層夾襖步出監(jiān)牢的瞬間婿失,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工啄寡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人哩照。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓挺物,卻偏偏與公主長得像,于是被迫代替她去往敵國和親飘弧。 傳聞我的和親對象是個殘疾皇子识藤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容