Android中FileProvider的各種場景應(yīng)用詳解
前言
有部分同學(xué)只要是上傳或者下載,只要用到了文件,不管三七二十一寫個(gè) FileProvider 再說。
不是每一種情況都需要使用 FileProvider 的,啥?你問行不行?有沒有毛???
這... 寫了確實(shí)可以,沒毛??!但是這沒有必要啊。
如果不需要FileProvider就不需要定義啊,如果定義了重復(fù)的 FileProvider,還會導(dǎo)致清單文件合并失敗,需要處理沖突,從而引出又一個(gè)問題,解決 FileProvider 的沖突問題,當(dāng)然這不是本文的重點(diǎn),網(wǎng)上也有解決方案。
這里我們只使用 FileProvider 來說,分析一下如下場景:
1.比如我們下載文件到SD卡,當(dāng)然我們一般都下載到download目錄下,那么使用這個(gè)文件,需要 FileProvider 嗎?
不需要!因?yàn)樗枪蚕砦募A中,并不是在沙盒中。
2.那我們把文件保存到沙盒中,比如 getExternalFilesDir 。那么我們使用這個(gè)沙盒中的文件,需要 FileProvider 嗎?
3.看情況,如果只是把此文件上傳到服務(wù)器,上傳到云平臺,也就是我們自己App使用自己的沙盒,是不需要 FileProvider 的
4.如果是想使用系統(tǒng)打開文件,或者傳遞給第三方App,那么是需要 FileProvider 的。
也就是說一般使用場景,我們只有在自己App沙盒中的文件,需要給別的App操作的時(shí)候,我們才需要使用 FileProvider 。
比較典型的例子是,下載Apk到自己的沙盒文件中,然后調(diào)用Android的Apk安裝器去安裝應(yīng)用(這是一個(gè)單獨(dú)的App),我們就需要 FileProvider 。
或者我們沙盒中的圖片,需要發(fā)送到第三方的App里面展示,我們需要 FileProvider 。
話不多說,我們從常規(guī)的使用與示例上來看看怎么使用,清楚它的一些小細(xì)節(jié)。
一、常規(guī)使用與定義
一般來說沒有什么特殊的需求,我們使用系統(tǒng)自帶的 FileProvider 類來定義即可。
我們再清單文件注冊我們的FileProvider
<provider
android:authorities="com.guadou.kt_demo.fileprovider"
android:name="androidx.core.content.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_path">
</meta-data>
</provider>
屬性的一些說明:
- authorities 是標(biāo)記我們這個(gè)ContentProvider的唯一標(biāo)識,是一個(gè)用于認(rèn)證的暗號,我們一般默認(rèn)使用包名+fileprovider來定義。(能不能使用別的,可以,abcd都行,但是沒必要)
- name 是具體的FileProvider類,如果是系統(tǒng)的,就用上面的這種,如果是自定義的,就寫自定義FileProvider的全類名。
- exported 是否限制其他應(yīng)用獲取此FileProvider。
- grantUriPermissions 是否授權(quán)其他應(yīng)用獲取訪問Uri權(quán)限,一般為true。
- meta-data 和下面的 name 都是固定的寫法,重點(diǎn)是 resource 需要自己實(shí)現(xiàn)規(guī)則,定義哪些私有文件會被提供訪問。
看看我們定義的file_path文件:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<root-path name="myroot" path="." />
<external-path name="external_file" path="." />
<files-path name="inner_app_file" path="." />
<cache-path name="inner_app_cache" path="." />
<external-files-path name="external_app_file" path="." />
<external-files-path name="log_file" path="log" />
<external-cache-path name="external_app_cache" path="." />
<external-cache-path name="naixiao_img" path="pos" />
</paths>
屬性的含義如下:
- root-path 從SD卡開始找 例如 storage/emulated/0/Android/data/com.guadou.kt_demo/cache/pos/naixiao-1122.jpg
- external-path 從外置SD卡開始 例如 Android/data/com.guadou.kt_demo/cache/pos/naixiao-1122.jpg
- external-files-path 外置沙盒file目錄 例如 pos/naixiao-1122.jpg (真實(shí)目錄在 Android/data/com.guadou.kt_demo/cache/pos/)
- external-cache-path 外置沙盒cache目錄 例如 naixiao-1122.jpg (真實(shí)目錄在 Android/data/com.guadou.kt_demo/cache/)
- files-path 和上面的同理,只是在內(nèi)置的data/data目錄下面
- cache-path 和上面的同理,只是在內(nèi)置的data/data目錄下面
總共使用的就這么幾個(gè),大家可以看到我的定義,它是可以重復(fù)定義的。
比我我用到的這兩個(gè),是的同樣類型的可以定義多個(gè),
<external-cache-path name="external_app_cache" path="." /> <external-cache-path name="naixiao_img" path="pos" />
如果我定義了兩個(gè)同類型的 external-cache-path ,他們的 name 你可以隨便取,叫abc都行,主要是path , 推薦大家如果想暴露根目錄就使用點(diǎn). , 如果想暴露指定的目錄就寫對應(yīng)的文件夾名稱。
比我我現(xiàn)在有一個(gè)圖片在這個(gè)目錄下
storage/emulated/0/Android/data/com.guadou.kt_demo/cache/pos/naixiao-1122.jpg
通過 FileProvider 獲取Uri 也是分優(yōu)先順序的。
比如我定義了pos的目錄,那么打印如下:
打印Uri:content://com.guadou.kt_demo.fileprovider/naixiao_img/naixiao-1122.jpg
那我們現(xiàn)在把pos的去掉,只要這個(gè)。
<external-cache-path name="external_app_cache" path="." />
那么打印就如下:
打印Uri:content://com.guadou.kt_demo.fileprovider/external_app_cache/pos/naixiao-1122.jpg
換了name,多了pos的路徑。
那我們都去掉呢?只保留外置SD卡和SD卡的規(guī)則。
<root-path name="myroot" path="." />
<external-path name="external_file" path="." />
那么打印就如下:
打印Uri:content://com.guadou.kt_demo.fileprovider/external_file/Android/data/com.guadou.kt_demo/cache/pos/naixiao-1122.jpg
就走到了外置SD卡的規(guī)則中去了。
那我們再去掉外置卡的規(guī)則。此時(shí)定義如下
<root-path name="myroot" path="." />
此時(shí)打印如下:
打印Uri:content://com.guadou.kt_demo.fileprovider/myroot/storage/emulated/0/Android/data/com.guadou.kt_demo/cache/pos/naixiao-1122.jpg
可以看到它的匹配規(guī)則是一層一層往上找的,那我們再去掉SD卡的規(guī)則呢。。。
那不就空了嗎,此時(shí)就崩潰報(bào)錯(cuò)了,這樣是真拿不到Uri了...
使用示例:
說到這里,我們還沒有真的使用 FileProvider ,下面我們以一個(gè)圖片實(shí)例為例子演示如何發(fā)送到系統(tǒng)的App
//測試FileProvider
fun fileProvider1() {
val drawable = drawable(R.drawable.chengxiao)
val bd: BitmapDrawable = drawable as BitmapDrawable
val bitmap = bd.bitmap
FilesUtils.getInstance().saveBitmap(bitmap, "naixiao-1122.jpg")
val filePath = FilesUtils.getInstance().sdpath + "naixiao-1122.jpg"
YYLogUtils.w("文件原始路徑:$filePath")
val uri = FileProvider.getUriForFile(commContext(), "com.guadou.kt_demo.fileprovider", File(filePath))
YYLogUtils.w("打印Uri:$uri")
//到系統(tǒng)中找打開對應(yīng)的文件
openFile(filePath, uri)
}
private fun openFile(path: String, uri: Uri) {
//取得文件擴(kuò)展名
val extension: String = path.substring(path.lastIndexOf(".") + 1)
//通過擴(kuò)展名找到mimeType
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
YYLogUtils.w("mimeType: $mimeType")
try {
//構(gòu)造Intent,啟動意圖,交由系統(tǒng)處理
startActivity(Intent().apply {
//臨時(shí)賦予讀寫權(quán)限
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
//表示用其它應(yīng)用打開
action = Intent.ACTION_VIEW
//給Intent 賦值
setDataAndType(uri, mimeType)
})
} catch (e: Exception) {
e.printStackTrace()
YYLogUtils.e("不能打開這種類型的文件")
}
}
很簡單的一個(gè)例子,我們把drawable中的一個(gè)圖片,保存到我們私有沙盒目錄中,目錄為
文件原始路徑:/storage/emulated/0/Android/data/com.guadou.kt_demo/cache/pos/naixiao-1122.jpg
我們通過 FileProvider 拿到 content://開頭的uri路徑。然后通過Intent匹配找到對于的第三方App來接收。
運(yùn)行結(jié)果如下:

打開了系統(tǒng)自帶的圖片查看器,還能編輯圖片,查看信息等。
那么打印就如下:
打印Uri:content://com.guadou.kt_demo.fileprovider/external_app_cache/pos/naixiao-1122.jpg
content 是 scheme。
com.guadou.kt_demo.fileprovider 即為我們在清單文件中定義的 authorities,即是我們的FileProvider的唯一表示,在接收的時(shí)候作為host。
這樣封裝之后,當(dāng)其他的App收到這個(gè)Uri就無法從這些信息得知我們的文件的真實(shí)路徑,相對有安全保障。
其他場景中,比如沙盒中的Apk文件想要安裝,也是一樣的流程,我們需要賦予讀寫權(quán)限,然后設(shè)置DataAndType即可。代碼的注釋很詳細(xì),大家可以參考參考。
此時(shí)我們都是發(fā)送了一個(gè)Intent,讓系統(tǒng)自己去匹配符合條件的Activity。那有沒有可能我們自己做一個(gè)App去匹配它。
這... 好像還真行。
二、能不能自定義接收文件?
其實(shí)我們仿造系統(tǒng)的App的做法,我們在自定義的Activity中加入指定Filter即可,比如這里我需要接收圖片,那么我定義如下的 intent-filter :
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ReceiveImageActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<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="image/*" />
</intent-filter>
</activity>
都是一些固定的寫法,我們在Activity上指明,它可以接收圖片數(shù)據(jù),此時(shí)我們再回到第一個(gè)App,發(fā)送圖片,看看運(yùn)行的效果:

之前還是圖片查看器,現(xiàn)在可以選擇我們自己的App來接收圖片數(shù)據(jù)了,但是我們?nèi)绾谓邮諗?shù)據(jù)呢?
其實(shí)都是一些固定的代碼,主要是拿到input流,然后操作流的處理。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_receive_image)
if (intent != null && intent.action == Intent.ACTION_VIEW) {
val uri = intent.data
YYLogUtils.w("uri: $uri")
if (uri != null && uri.scheme != null && uri.scheme == "content") {
val fis = contentResolver.openInputStream(uri)
if (fis != null) {
val bitmap = BitmapFactory.decodeStream(fis)
//展示
if (bitmap != null) {
val ivReveiverShow = findViewById<ImageView>(R.id.iv_reveiver_show)
ivReveiverShow.setImageBitmap(bitmap)
}
}
}
}
}
最簡單的做法,直接根據(jù)uri打開輸入流,然后我們可以通過 BitmapFactory 就可以拿到 Bitmap了,就能展示圖片到ImageView上面。
效果如圖:

甚至我們拿到了 input 流,我們還能對流進(jìn)行copy 操作,把你的圖片保存到我自己的沙盒目錄中,例如:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_receive_image)
if (intent != null && intent.action == Intent.ACTION_VIEW) {
val uri = intent.data
YYLogUtils.w("uri: $uri")
if (uri != null && uri.scheme != null && uri.scheme == "content") {
val fis = contentResolver.openInputStream(uri)
if (fis != null) {
val inBuffer = fis.source().buffer()
val outFile = File(getExternalFilesDir("xiaoxiao"), "naixiao5566.jpg")
outFile.sink().buffer().use {
it.writeAll(inBuffer)
inBuffer.close()
}
YYLogUtils.w("存放的路徑:${outFile.absolutePath}")
//展示
val ivReveiverShow = findViewById<ImageView>(R.id.iv_reveiver_show)
ivReveiverShow.extLoad(outFile.absolutePath)
}
}
}
}
保存到自己的沙盒文件之后,我們看一看效果:

好像還真的能行,秀啊。
那此時(shí)有人還會有一個(gè)疑問,你這方法都是我主動的發(fā)送給別人去展示,去操作!這都不是事,關(guān)鍵是能不能讓別人主動的來操作、玩弄我的沙盒文件?
比如我做的App想獲取微信,支付寶這些別人的App的沙盒中的圖片?行不行?有沒有方法可以做到?
這...,你別逗我了。
三、能不能主動查詢對方的沙盒?
轉(zhuǎn)頭一想,好像還真行,有操作空間啊... 既然 FileProvider 是繼承自 ContentProvider 。那憑什么我們的App都能獲取到別人App的數(shù)據(jù)庫了,不能獲取別人的沙盒文件呢?那數(shù)據(jù)庫文件不也存在沙盒中么?
例如聯(lián)系人App,我們開發(fā)的第三方App可以通過 ContentProvider 獲取到聯(lián)系人App中的聯(lián)系人數(shù)據(jù),那么只要第三方的App定義好對應(yīng)的 ContentProvider 我不就能獲取到它沙盒的文件了嗎?
說到就做,我們先把FileProvider設(shè)置為可訪問
<provider
android:name=".MyFileProvider"
android:authorities="com.guadou.kt_demo.fileprovider"
android:exported="true"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_path">
</meta-data>
</provider>
是的,android:exported="true" 設(shè)置成功之后我們直接通過 contentResolver 去查詢不就好了嗎?
先運(yùn)行一下試試! 運(yùn)行就崩了?
什么鬼哦,看看FileProvider的代碼,原來不允許開放

原來 FileProvider的 exported 和 grantUriPermissions 都是指定的寫法,不能改變,并且不允許暴露,不允許給別的App主動訪問!
這和我們的需求不符合啊,我就要主動訪問,既然你不行,那我不用你行了吧!我繼承 ContentProvider 行了吧!我自己實(shí)現(xiàn)文件獲取、Cursor封裝行了吧!
不皮了,其實(shí)我們直接通過繼承 ContentProvider 并且允許 exported ,然后我們通過自己實(shí)現(xiàn)的query方法,返回指定的Cursor信息,就可以實(shí)現(xiàn)!
部分代碼如下:
public class MyFileProvider extends ContentProvider {
@Override
public void attachInfo(Context context, ProviderInfo info) {
super.attachInfo(context, info);
mStrategy = getPathStrategy(context, info.authority);
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
YYLogUtils.w("走到query方法");
final File file = mStrategy.getFileForUri(uri);
YYLogUtils.w("file:" + file);
if (!file.exists()) {
return null;
}
boolean directory = file.isDirectory();
if (directory) {
YYLogUtils.w("說明是文件夾?。?);
File[] files = file.listFiles();
for (File childFile : files) {
if (childFile.isFile()) {
String name = childFile.getName();
String path = childFile.getPath();
long size = childFile.length();
Uri uriForFile = mStrategy.getUriForFile(childFile);
YYLogUtils.w("name:" + name + " path:" + path + " size: " + size +" uriForFile:"+uriForFile);
}
}
//自己遍歷封裝Cursor實(shí)現(xiàn)
return null;
} else {
YYLogUtils.w("說明是文件?。?);
if (projection == null) {
projection = COLUMNS;
}
String[] cols = new String[projection.length];
Object[] values = new Object[projection.length];
int i = 0;
for (String col : projection) {
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
cols[i] = OpenableColumns.DISPLAY_NAME;
values[i++] = file.getName();
} else if (OpenableColumns.SIZE.equals(col)) {
cols[i] = OpenableColumns.SIZE;
values[i++] = file.length();
}
}
cols = copyOf(cols, i);
values = copyOf(values, i);
final MatrixCursor cursor = new MatrixCursor(cols, 1);
cursor.addRow(values);
return cursor;
}
}
}
我簡單的做了文件和文件夾的處理,并不完整,如果是文件我們可以直接返回一個(gè)簡單的cursor,如果是文件夾需要大家自己拼接子文件的cursor并返回。
接下來我們看看其他App如何主動這些文件,在另一個(gè)App中我們先加上權(quán)限:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.hongyegroup.receiver">
<queries>
<provider android:authorities="com.guadou.kt_demo.fileprovider" />
</queries>
...
</manifest>
然后我們直接使用 contentResolver.query
private fun queryFiles() {
val uri = Uri.parse("content://com.guadou.kt_demo.fileprovider/external_app_cache/pos/naixiao-1122.jpg")
val cursor = contentResolver.query(uri, null, null, null, null)
if (cursor != null) {
while (cursor.moveToNext()) {
val fileName = cursor.getString(cursor.getColumnIndex("_display_name"));
val size = cursor.getLong(cursor.getColumnIndex("_size"));
YYLogUtils.w("name: $fileName size: $size")
Toast.makeText(this, "name: $fileName size: $size", Toast.LENGTH_SHORT).show()
}
cursor.close()
} else {
YYLogUtils.w("cursor-result: 為空啊")
Toast.makeText(this, "cursor-result: 為空啊", Toast.LENGTH_SHORT).show()
}
}
如果我們知道它的指定文件Uri,我們可以通過query查詢到文件的一些基本信息。具體是哪些信息,需要對方提供和定義。
如果想操作對方的文件,由于我們已經(jīng)拿到了對方的Uri,我們可以直接通過inputStream來操作,例如:
val fis = contentResolver.openInputStream(uri)
if (fis != null) {
val inBuffer = fis.source().buffer()
val outFile = File(getExternalFilesDir(null), "abc")
outFile.sink().buffer().use {
it.writeAll(inBuffer)
inBuffer.close()
}
YYLogUtils.w("保存文件成功")
}
這些都是簡單的基本操作,重點(diǎn)是如果我不知道具體的文件呢?
我就想把對方App的沙盒中的文件夾下面的全部文件都拿到,行不行?
行!只要對方App配合就行,例如:
private fun queryFiles() {
val uri = Uri.parse("content://com.guadou.kt_demo.fileprovider/external_app_cache/pos/")
val cursor = contentResolver.query(uri, null, null, null, null)
if (cursor != null) {
while (cursor.moveToNext()) {
val fileName = cursor.getString(cursor.getColumnIndex("_display_name"));
val size = cursor.getLong(cursor.getColumnIndex("_size"));
val uri = cursor.getString(cursor.getColumnIndex("uri"));
val fileUri = Uri.parse(uri)
//就可以使用IO或者BitmapFactory來操作流了
YYLogUtils.w("name: $fileName size: $size")
Toast.makeText(this, "name: $fileName size: $size", Toast.LENGTH_SHORT).show()
}
cursor.close()
} else {
YYLogUtils.w("cursor-result: 為空啊")
Toast.makeText(this, "cursor-result: 為空啊", Toast.LENGTH_SHORT).show()
}
}
這樣就是把對方外置SD卡下面的cache目錄下的pos目錄下的全部文件拿到手,當(dāng)然了,這個(gè)需要對方App封裝對應(yīng)的cursor才行哦。
打印的Log如下:

只要對方封裝的Cursor,我們可以把名字,大小,uri等信息都封裝到Cursor中,提供給對方獲取。
總結(jié)
FileProvider的主要應(yīng)用場景就是分享,把自己沙盒中的文件分享,主動提供給其他匹配的App去使用。
使用其他App的圖片?查詢了目前市場上的主流App,微信,支付寶,閑魚,美團(tuán),等App,例如在保存文件的時(shí)候都沒有存在自己的沙盒中了,都是默認(rèn)在DCIM或Pictures中,并存入 MediaStore 保存到圖庫中。
這樣就算公共目錄,無需FileProvider,大家直接通過 MediaStore 就能獲取和使用。
而如果想主動訪問其他App的沙盒文件,則需要對方App全方位配合,一般用于自家App的全家桶之類的應(yīng)用。相對來說相對應(yīng)用場景比較少。
不是做不到,只是大家覺得沒有必要而已,畢竟定義和使用相對復(fù)雜,并且有暴露風(fēng)險(xiǎn),被攻擊的風(fēng)險(xiǎn)等。
本文全部代碼均以開源,源碼在此
以上就是Android中FileProvider的各種場景應(yīng)用詳解的詳細(xì)內(nèi)容,更多關(guān)于Android FileProvider應(yīng)用場景的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android RecyclerView item選中放大被遮擋問題詳解
這篇文章主要介紹了Android RecyclerView item選中放大被遮擋問題詳解,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-04-04
Android自定義View實(shí)現(xiàn)帶數(shù)字的進(jìn)度條實(shí)例代碼
這篇文章主要介紹了Android自定義View實(shí)現(xiàn)帶數(shù)字的進(jìn)度條實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2016-03-03
Android 優(yōu)化Handler防止內(nèi)存泄露
這篇文章主要介紹了Android 優(yōu)化Handler防止內(nèi)存泄露的相關(guān)資料,這里提供實(shí)例幫助大家理解掌握這樣的內(nèi)容,需要的朋友可以參考下2017-09-09
21天學(xué)習(xí)android開發(fā)教程之SurfaceView與多線程的混搭
21天學(xué)習(xí)android開發(fā)教程之SurfaceView與多線程的混搭,感興趣的小伙伴們可以參考一下2016-02-02
Android實(shí)現(xiàn)支持所有View的通用的下拉刷新控件
這篇文章主要介紹了Android實(shí)現(xiàn)支持所有View的通用的下拉刷新控件的相關(guān)資料,需要的朋友可以參考下2016-06-06
Android ListView的Item點(diǎn)擊效果的定制
這篇文章主要介紹了Android ListView的Item點(diǎn)擊效果的定制的相關(guān)資料,需要的朋友可以參考下2017-07-07
Android實(shí)現(xiàn)View滑動效果的6種方法
這篇文章主要介紹了Android實(shí)現(xiàn)View滑動的6種方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-03-03
android使用DataBinding來設(shè)置空狀態(tài)
本篇文章主要介紹了android使用DataBinding來設(shè)置空狀態(tài),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-03-03

