前言
存儲(chǔ)適配系列文章:
Android-存儲(chǔ)基礎(chǔ)
Android-10隐砸、11-存儲(chǔ)完全適配(上)
Android-10票摇、11-存儲(chǔ)完全適配(下)
Android-FileProvider-輕松掌握
之前在分析Android 存儲(chǔ)相關(guān)知識(shí)點(diǎn)的時(shí)候,有同學(xué)提出希望也分析一下FileProvider鱼填,那時(shí)忙于總結(jié)線程并發(fā)知識(shí)點(diǎn),并沒(méi)有立即著手分享。本次驶沼,將著重分析Android 應(yīng)用之間如何使用第三方應(yīng)用打開(kāi)文件,如何分享文件給第三方應(yīng)用争群。
通過(guò)本篇文章回怜,你將了解到:
1、Android 應(yīng)用間共享文件
2换薄、FileProvider 應(yīng)用與原理
3玉雾、FileProvider Uri構(gòu)造與解析
1翔试、Android 應(yīng)用間共享文件
共享基礎(chǔ)
提到文件共享,首先想到就是在本地磁盤上存放一個(gè)文件抹凳,多個(gè)應(yīng)用都可以訪問(wèn)它遏餐,如下:
理想狀態(tài)下只要知道了文件的存放路徑,那么各個(gè)應(yīng)用都可以讀寫它赢底。
比如相冊(cè)里的圖片存放目錄:/sdcard/DCIM/失都、/sdcard/Pictures/ 。
再比如相冊(cè)里的視頻存放目錄:/sdcard/DCIM/幸冻、/sdcard/Movies/粹庞。
共享方式
一個(gè)常見(jiàn)的應(yīng)用場(chǎng)景:
應(yīng)用A里檢索到一個(gè)文件my.txt,它無(wú)法打開(kāi)洽损,于是想借助其它應(yīng)用打開(kāi)庞溜,這個(gè)時(shí)候它需要把待打開(kāi)的文件路徑告訴其它應(yīng)用。
假設(shè)應(yīng)用B可以打開(kāi)my.txt碑定,那么應(yīng)用A如何把路徑傳遞給應(yīng)用B呢流码,這就涉及到了進(jìn)程間通信。我們知道Android進(jìn)程間通信主要手段是Binder延刘,而四大組件的通信也是依靠Binder漫试,因此我們應(yīng)用間傳遞路徑可以依靠四大組件。
可以看出碘赖,Activity/Service/Broadcast 可以傳遞Intent驾荣,而ContentProvider傳遞Uri,實(shí)際上Intent 里攜帶了Uri變量普泡,因此四大組件之間可以傳遞Uri播掷,而路徑就可以存放在Uri里。
2撼班、FileProvider 應(yīng)用與原理
以使用其它應(yīng)用打開(kāi)文件為例歧匈,分別闡述Android 7.0 前后的不同點(diǎn)。
Android 7.0 之前使用
上面說(shuō)到了傳遞路徑可以通過(guò)Uri砰嘁,來(lái)看看如何使用:
private void openByOtherForN() {
Intent intent = new Intent();
//指定Action眯亦,使用其它應(yīng)用打開(kāi)
intent.setAction(Intent.ACTION_VIEW);
//通過(guò)路徑,構(gòu)造Uri
Uri uri = Uri.fromFile(new File(external_filePath));
//設(shè)置Intent般码,附帶Uri
intent.setData(uri);
//跨進(jìn)程傳遞Intent
startActivity(intent);
}
其中
- external_filePath="/storage/emulated/0/fish/myTxt.txt"
- 構(gòu)造為uri 后uriString="file:///storage/emulated/0/fish/myTxt.txt"
可以看出,文件路徑前多了"file:///"字符串乱顾。
而接收方在收到Intent后板祝,拿出Uri,通過(guò):
filePath = uri.getEncodedPath() 拿到發(fā)送方發(fā)送的原始路徑后走净,即可讀寫文件券时。
然而此種構(gòu)造Uri方式在Android7.0(含)之后被禁止了孤里,若是使用則拋出異常:
可以看出,Uri.fromFile 構(gòu)造方式的缺點(diǎn):
1橘洞、發(fā)送方傳遞的文件路徑接收方完全知曉捌袜,一目了然,沒(méi)有安全保障炸枣。
2虏等、發(fā)送方傳遞的文件路徑接收方可能沒(méi)有讀取權(quán)限,導(dǎo)致接收異常适肠。
Android 7.0(含)之后的使用
先想想霍衫,若是我們自己操刀,如何規(guī)避以上兩個(gè)問(wèn)題呢侯养?
針對(duì)第一個(gè)問(wèn)題:
可以將具體路徑替換為另一個(gè)字符串敦跌,類似以前密碼本的感覺(jué),比如:
"/storage/emulated/0/fish/myTxt.txt" 替換為"myfile/Txt.txt"逛揩,這樣接收方收到文件路徑完全不知道原始文件路徑是咋樣的柠傍。
不過(guò)這也引入了另一個(gè)額外的問(wèn)題:接收方不知道真實(shí)路徑,如何讀取文件呢?
針對(duì)第二個(gè)問(wèn)題
既然不確定接收方是否有打開(kāi)文件權(quán)限辩稽,那么是否由發(fā)送方打開(kāi)惧笛,然后將流傳遞給接收方就可以了呢?
Android 7.0(含)之后引入了FileProvider搂誉,可以解決上述兩個(gè)問(wèn)題徐紧。
FileProvider 應(yīng)用
先來(lái)看看如何使用FileProvider 來(lái)傳遞路徑。
細(xì)分為四個(gè)步驟:
一:定義FileProvider 子類
public class MyFileProvider extends FileProvider {
}
定義一個(gè)空的類炭懊,繼承自FileProvider并级,而FileProvider 繼承自ContentProvider。
注:FileProvider 需要引入AndroidX
二:AndroidManifest 里聲明FileProvider
既然是ContentProvider侮腹,那么需要像Activity一樣在AndroidManifest.xml里聲明:
<provider
android:authorities="com.fish.fileprovider"
android:name=".fileprovider.MyFileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_path">
</meta-data>
</provider>
字段解釋如下:
1嘲碧、android:authorities 標(biāo)識(shí)ContentProvider的唯一性,可以自己任意定義父阻,最好是全局唯一的愈涩。
2、android:name 是指之前定義的FileProvider 子類加矛。
3履婉、android:exported="false" 限制其他應(yīng)用獲取Provider。
4斟览、android:grantUriPermissions="true" 授予其它應(yīng)用訪問(wèn)Uri權(quán)限毁腿。
5、meta-data 囊括了別名應(yīng)用表。
5.1已烤、android:name 這個(gè)值是固定的鸠窗,表示要解析file_path。
5.2胯究、android:resource 自己定義實(shí)現(xiàn)的映射表
三:路徑映射表
可以看出稍计,F(xiàn)ileProvider需要讀取映射表。
在/res/ 下建立xml 文件夾裕循,然后再創(chuàng)建對(duì)應(yīng)的映射表(xml)臣嚣,最終路徑如下:/res/xml/file_path.xml。
內(nèi)容如下:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<root-path name="myroot" path="." />
<external-path name="external_file" path="fish" />
<external-files-path name="external_app_file" path="myfile" />
<external-cache-path name="external_app_cache" path="mycache/doc/" />
<files-path name="inner_app_file" path="." />
<cache-path name="inner_app_cache" path="." />
</paths>
字段解釋如下:
1费韭、root-path 標(biāo)簽表示要給根目錄下的子目錄取別名(包括內(nèi)部存儲(chǔ)茧球、自帶外部存儲(chǔ)、擴(kuò)展外部存儲(chǔ)星持,統(tǒng)稱用"/"表示)抢埋,path 屬性表示需要被更改的目錄名,其值為:"."督暂,表示不區(qū)分目錄揪垄,name 屬性表示將path 目錄更改后的別名。
2逻翁、假若有個(gè)文件路徑:/storage/emulated/0/fish/myTxt.txt饥努,而我們只配置了root-path 標(biāo)簽,那么最終該文件路徑被替換為:/myroot/storage/emulated/0/fish/myTxt.txt八回。
可以看出酷愧,因?yàn)閜ath=".",因此任何目錄前都被追加了myroot缠诅。
剩下的external-path等標(biāo)簽對(duì)應(yīng)的目錄如下:
1溶浴、external-path--->Environment.getExternalStorageDirectory(),如/storage/emulated/0/fish
2管引、external-files-path--->ContextCompat.getExternalFilesDirs(context, null)士败。
3、external-cache-path--->ContextCompat.getExternalCacheDirs(context)褥伴。
4谅将、files-path--->context.getFilesDir()。
5重慢、cache-path--->context.getCacheDir()饥臂。
你可能已經(jīng)發(fā)現(xiàn)了,這些標(biāo)簽所代表的目錄有重疊的部分似踱,在替換別名的時(shí)候如何選擇呢隅熙?答案是:選擇最長(zhǎng)匹配的志衣。
假設(shè)我們映射表里只定義了root-path與external-path,分別對(duì)應(yīng)的目錄為:
root-path--->/
external-path--->/storage/emulated/0/
現(xiàn)在要傳遞的文件路徑為:/storage/emulated/0/fish/myTxt.txt猛们。需要給這個(gè)文件所在目錄取別名,因此會(huì)遍歷映射表找到最長(zhǎng)匹配該目錄的標(biāo)簽狞洋,顯然external-path 所表示的/storage/emulated/0/ 與文件目錄最為匹配弯淘,因此最后文件路徑被替換為:/external_file/myTxt.txt
四:使用FileProvider 構(gòu)造路徑
映射表建立好之后,接著就需要構(gòu)造路徑吉懊。
private void openByOther() {
//取得文件擴(kuò)展名
String extension = external_filePath.substring(external_filePath.lastIndexOf(".") + 1);
//通過(guò)擴(kuò)展名找到mimeType
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
//構(gòu)造Intent
Intent intent = new Intent();
//賦予讀寫權(quán)限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
//表示用其它應(yīng)用打開(kāi)
intent.setAction(Intent.ACTION_VIEW);
File file = new File(external_filePath);
//第二個(gè)參數(shù)表示要用哪個(gè)ContentProvider庐橙,這個(gè)唯一值在AndroidManifest.xml里定義了
//若是沒(méi)有定義MyFileProvider,可直接使用FileProvider替代
Uri uri = MyFileProvider.getUriForFile(this, "com.fish.fileprovider", file);
//給Intent 賦值
intent.setDataAndType(uri, mimeType);
try {
//交由系統(tǒng)處理
startActivity(intent);
} catch (Exception e) {
//若是沒(méi)有其它應(yīng)用能夠接收打開(kāi)此種mimeType借嗽,則拋出異常
Toast.makeText(this, e.getLocalizedMessage(),Toast.LENGTH_SHORT).show();
}
}
/storage/emulated/0/fish/myTxt.txt 最終構(gòu)造為:content://com.fish.fileprovider/external_file/myTxt.txt
對(duì)于私有目錄:/data/user/0/com.example.androiddemo/files/myTxt.txt 最終構(gòu)造為:
content://com.fish.fileprovider/inner_app_file/myTxt.txt
可以看出添加了:
content 作為scheme态鳖;
com.fish.fileprovider 即為我們定義的 authorities,作為host恶导;
如此構(gòu)造后浆竭,第三方應(yīng)用收到此Uri后,并不能從路徑看出我們傳遞的真實(shí)路徑惨寿,這就解決了第一個(gè)問(wèn)題:
發(fā)送方傳遞的文件路徑接收方完全知曉邦泄,一目了然,沒(méi)有安全保障裂垦。
3顺囊、FileProvider Uri構(gòu)造與解析
Uri 構(gòu)造輸入流
發(fā)送方將Uri交給系統(tǒng),系統(tǒng)找到有能力處理該Uri的應(yīng)用蕉拢。發(fā)送方A需要?jiǎng)e的應(yīng)用打開(kāi)myTxt.txt 文件特碳,假設(shè)應(yīng)用B具有能夠打開(kāi)文本文件的能力,并且也愿意接收別人傳遞過(guò)來(lái)的路徑晕换,那么它需要在AndroidManifest里做如下聲明:
<activity android:name="com.fish.fileprovider.ReceiveActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW"></action>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="content"/>
<data android:scheme="file"/>
<data android:scheme="http"/>
<data android:mimeType="text/*"></data>
</intent-filter>
</activity>
android.intent.action.VIEW 表示接收別的應(yīng)用打開(kāi)文件的請(qǐng)求午乓。
android:mimeType 表示其具有打開(kāi)某種文件的能力,text/* 表示只接收文本類型的打開(kāi)請(qǐng)求届巩。
當(dāng)聲明了上述內(nèi)容后硅瞧,該應(yīng)用就會(huì)出現(xiàn)在系統(tǒng)的選擇彈框里,當(dāng)用戶點(diǎn)擊彈框里的該應(yīng)用時(shí)恕汇,ReceiveActivity 將會(huì)被調(diào)用腕唧。我們知道,傳遞過(guò)來(lái)的Uri被包裝在Intent里瘾英,因此ReceiveActivity 需要處理Intent枣接。
private void handleIntent() {
Intent intent = getIntent();
if (intent != null) {
if (intent.getAction().equals(Intent.ACTION_VIEW)) {
//從Intent里獲取uri
uri = intent.getData();
String content = handleUri(uri);
if (!TextUtils.isEmpty(content)) {
tvContent.setText("打開(kāi)文件內(nèi)容:" + content);
}
}
}
}
private String handleUri(Uri uri) {
if (uri == null)
return null;
String scheme = uri.getScheme();
if (!TextUtils.isEmpty(scheme)) {
if (scheme.equals("content")) {
try {
//從uri構(gòu)造流
InputStream inputStream = getContentResolver().openInputStream(uri);
try {
//有流之后即可讀取內(nèi)容
byte[] content = new byte[inputStream.available()];
inputStream.read(content);
return new String(content);
} catch (IOException e) {
e.printStackTrace();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
return null;
}
從Intent里拿到Uri,再通過(guò)Uri構(gòu)造輸入流缺谴,最終從輸入流里讀取文件內(nèi)容但惶。
至此,應(yīng)用A通過(guò)FileProvider可將其能夠訪問(wèn)的任意路徑的文件傳遞給應(yīng)用B,應(yīng)用B能夠讀取文件并展示膀曾。
看到這里县爬,你可能已經(jīng)發(fā)現(xiàn)了:還沒(méi)有解決第二個(gè)問(wèn)題呢:發(fā)送方傳遞的文件路徑接收方可能沒(méi)有讀取權(quán)限,導(dǎo)致接收異常添谊。
這就需要從getContentResolver().openInputStream(uri)說(shuō)起:
#ContentResolver.java
public final @Nullable InputStream openInputStream(@NonNull Uri uri)
throws FileNotFoundException {
Preconditions.checkNotNull(uri, "uri");
String scheme = uri.getScheme();
if (SCHEME_ANDROID_RESOURCE.equals(scheme)) {
...
} else if (SCHEME_FILE.equals(scheme)) {
//file開(kāi)頭
} else {
//content開(kāi)頭 走這
AssetFileDescriptor fd = openAssetFileDescriptor(uri, "r", null);
try {
//從文件描述符獲取輸入流
return fd != null ? fd.createInputStream() : null;
} catch (IOException e) {
throw new FileNotFoundException("Unable to create stream");
}
}
}
public final @Nullable AssetFileDescriptor openAssetFileDescriptor(@NonNull Uri uri,
@NonNull String mode, @Nullable CancellationSignal cancellationSignal)
throws FileNotFoundException {
...
//根據(jù)scheme 區(qū)分不同的協(xié)議
String scheme = uri.getScheme();
if (SCHEME_ANDROID_RESOURCE.equals(scheme)) {
//資源文件
} else if (SCHEME_FILE.equals(scheme)) {
//file 開(kāi)頭
} else {
//content 開(kāi)頭
if ("r".equals(mode)) {
return openTypedAssetFileDescriptor(uri, "*/*", null, cancellationSignal);
} else {
...
}
}
}
public final @Nullable AssetFileDescriptor openTypedAssetFileDescriptor(@NonNull Uri uri,
@NonNull String mimeType, @Nullable Bundle opts,
@Nullable CancellationSignal cancellationSignal) throws FileNotFoundException {
...
//找到FileProvider IPC 調(diào)用
IContentProvider unstableProvider = acquireUnstableProvider(uri);
try {
try {
//IPC 調(diào)用财喳,返回文件描述符
fd = unstableProvider.openTypedAssetFile(
mPackageName, uri, mimeType, opts, remoteCancellationSignal);
if (fd == null) {
// The provider will be released by the finally{} clause
return null;
}
} catch (DeadObjectException e) {
...
}
...
//構(gòu)造AssetFileDescriptor
return new AssetFileDescriptor(pfd, fd.getStartOffset(),
fd.getDeclaredLength());
} catch (RemoteException e) {
...
}
}
以上是應(yīng)用B的調(diào)用流程,最終拿到應(yīng)用A的FileProvider斩狱,拿到FileProvider 后即可進(jìn)行IPC調(diào)用耳高。
應(yīng)用B發(fā)起了IPC,來(lái)看看應(yīng)用A如何響應(yīng)這動(dòng)作的:
#ContentProviderNative.java
//Binder調(diào)用此方法
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
throws RemoteException {
case OPEN_TYPED_ASSET_FILE_TRANSACTION:
{
...
fd = openTypedAssetFile(callingPkg, url, mimeType, opts, signal);
}
}
#ContentProvider.java
@Override
public AssetFileDescriptor openTypedAssetFile(String callingPkg, Uri uri, String mimeType,
Bundle opts, ICancellationSignal cancellationSignal) throws FileNotFoundException {
...
try {
return mInterface.openTypedAssetFile(
uri, mimeType, opts, CancellationSignal.fromTransport(cancellationSignal));
} catch (RemoteException e) {
...
} finally {
...
}
}
public @Nullable AssetFileDescriptor openAssetFile(@NonNull Uri uri, @NonNull String mode)
throws FileNotFoundException {
ParcelFileDescriptor fd = openFile(uri, mode);
return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null;
}
可以看出所踊,最后調(diào)用了openFile()方法泌枪,而FileProvider重寫了該方法:
#ParcelFileDescriptor.java
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
throws FileNotFoundException {
//解析uri,從里面拿出對(duì)應(yīng)的路徑
final File file = mStrategy.getFileForUri(uri);
final int fileMode = modeToMode(mode);
//構(gòu)造ParcelFileDescriptor
return ParcelFileDescriptor.open(file, fileMode);
}
ParcelFileDescriptor 持有FileDescriptor秕岛,可以跨進(jìn)程傳輸碌燕。
重點(diǎn)是mStrategy.getFileForUri(uri),如何通過(guò)Uri找到path瓣蛀,代碼很簡(jiǎn)單陆蟆,就不貼了,僅用圖展示惋增。
關(guān)于IPC與四大組件相關(guān)可移步以下文章:
Android 四大組件通信核心
Android IPC 之Binder基礎(chǔ)
Uri與Path互轉(zhuǎn)
Path 轉(zhuǎn)Uri
回到最初應(yīng)用A如何將path構(gòu)造為Uri:
應(yīng)用A在啟動(dòng)的時(shí)候叠殷,會(huì)掃描AndroidManifest.xml 里的FileProvider,并讀取映射表構(gòu)造為一個(gè)Map:
這個(gè)Map的Key 為映射表里的別名诈皿,而Value對(duì)應(yīng)需要替換的目錄林束。
還是以/storage/emulated/0/fish/myTxt.txt 為例:
當(dāng)調(diào)用MyFileProvider.getUriForFile(xx)時(shí),遍歷Map稽亏,找到最匹配條目壶冒,最匹配的即為external_file。因此會(huì)用external_file 代替/storage/emulated/0/fish/截歉,最終形成的Uri為:content://com.fish.fileprovider/external_file/myTxt.txt
Uri 轉(zhuǎn)Path
構(gòu)造了Uri傳遞給應(yīng)用B胖腾,應(yīng)用B又通過(guò)Uri構(gòu)造輸入流,構(gòu)造輸入流的過(guò)程由應(yīng)用A完成瘪松,因此A需要將Uri轉(zhuǎn)為Path:
A先將Uri分離出external_file/myTxt.txt咸作,然后通過(guò)external_file 從Map里找到對(duì)應(yīng)Value 為:/storage/emulated/0/fish/,最后將myTxt.txt拼接宵睦,形成的路徑為:
/storage/emulated/0/fish/myTxt.txt
可以看出记罚,Uri成功轉(zhuǎn)為了Path。
現(xiàn)在來(lái)梳理整個(gè)流程:
1壳嚎、應(yīng)用A使用FileProvider通過(guò)Map(映射表)將Path轉(zhuǎn)為Uri桐智,通過(guò)IPC 傳遞給應(yīng)用B末早。
2、應(yīng)用B使用Uri通過(guò)IPC獲取應(yīng)用A的FileProvider说庭。
3然磷、應(yīng)用A使用FileProvider通過(guò)映射表將Uri轉(zhuǎn)為Path,并構(gòu)造出文件描述符刊驴。
4样屠、應(yīng)用A將文件描述符返回給應(yīng)用B,應(yīng)用B就可以讀取應(yīng)用A發(fā)送的文件了缺脉。
由以上可知,不管應(yīng)用B是否有存儲(chǔ)權(quán)限悦穿,只要應(yīng)用A有權(quán)限就行攻礼,因?yàn)閷?duì)文件的訪問(wèn)都是通過(guò)應(yīng)用A完成的,這就回答了第二個(gè)問(wèn)題:發(fā)送方傳遞的文件路徑接收方可能沒(méi)有讀取權(quán)限栗柒,導(dǎo)致接收異常礁扮。
以上以打開(kāi)文件為例闡述了FileProvider的應(yīng)用,實(shí)際上分享文件也是類似的過(guò)程瞬沦。
當(dāng)然太伊,從上面可以看出FileProvider構(gòu)造需要好幾個(gè)步驟,還需要區(qū)分不同Android版本的差異逛钻,因此將這幾個(gè)步驟抽象為一個(gè)簡(jiǎn)單的庫(kù)僚焦,外部直接調(diào)用對(duì)應(yīng)的方法即可。
引入庫(kù)步驟:
1曙痘、project build.gradle 里加入:
allprojects {
repositories {
...
//庫(kù)是發(fā)布在jitpack上芳悲,因此需要指定位置
maven { url 'https://jitpack.io' }
}
}
2、在module build.gradle 里加入:
dependencies {
...
//引入EasyStorage庫(kù)
implementation 'com.github.fishforest:EasyStorage:1.0.1'
}
3边坤、使用方式:
EasyFileProvider.fillIntent(this, new File(filePath), intent, true);
如上一行代碼搞定名扛。
效果如下:
本文基于Android 10.0
演示代碼與庫(kù)源碼 若是有幫助,給github 點(diǎn)個(gè)贊唄~