別濫用FileProvider了,Android中FileProvider的各種場景應用

語言: CN / TW / HK

theme: juejin highlight: a11y-dark


我正在參與掘金創作者訓練營第6期,點選瞭解活動詳情

前言

有部分同學只要是上傳或者下載,只要用到了檔案,不管三七二十一寫個 FileProvider 再說。

不是每一種情況都需要使用 FileProvider 的,啥?你問行不行?有沒有毛病?

這... 寫了確實可以,沒毛病!但是這沒有必要啊。

如果不需要FileProvider就不需要定義啊,如果定義了重複的 FileProvider,還會導致清單檔案合併失敗,需要處理衝突,從而引出又一個問題,解決 FileProvider 的衝突問題,當然這不是本文的重點,網上也有解決方案。

這裡我們只使用 FileProvider 來說,分析一下如下場景:

1.比如我們下載檔案到SD卡,當然我們一般都下載到download目錄下,那麼使用這個檔案,需要 FileProvider 嗎?

不需要!因為他是共享資料夾中,並不是在沙盒中。

2.那我們把檔案儲存到沙盒中,比如 getExternalFilesDir 。那麼我們使用這個沙盒中的檔案,需要 FileProvider 嗎?

3.看情況,如果只是把此檔案上傳到伺服器,上傳到雲平臺,也就是我們自己App使用自己的沙盒,是不需要 FileProvider 的

4.如果是想使用系統開啟檔案,或者傳遞給第三方App,那麼是需要 FileProvider 的。

也就是說一般使用場景,我們只有在自己App沙盒中的檔案,需要給別的App操作的時候,我們才需要使用 FileProvider 。

比較典型的例子是,下載Apk到自己的沙盒檔案中,然後呼叫Android的Apk安裝器去安裝應用(這是一個單獨的App),我們就需要 FileProvider 。

或者我們沙盒中的圖片,需要傳送到第三方的App裡面展示,我們需要 FileProvider 。

話不多說,我們從常規的使用與示例上來看看怎麼使用,清楚它的一些小細節。

一、常規使用與定義

一般來說沒有什麼特殊的需求,我們使用系統自帶的 FileProvider 類來定義即可。

我們再清單檔案註冊我們的FileProvider

xml <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>

屬性的一些說明:

  1. authorities 是標記我們這個ContentProvider的唯一標識,是一個用於認證的暗號,我們一般預設使用包名+fileprovider來定義。(能不能使用別的,可以,abcd都行,但是沒必要)
  2. name 是具體的FileProvider類,如果是系統的,就用上面的這種,如果是自定義的,就寫自定義FileProvider的全類名。
  3. exported 是否限制其他應用獲取此FileProvider。
  4. grantUriPermissions 是否授權其他應用獲取訪問Uri許可權,一般為true。
  5. meta-data 和下面的 name 都是固定的寫法,重點是 resource 需要自己實現規則,定義哪些私有檔案會被提供訪問。

看看我們定義的file_path檔案: ```xml

<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" />

```

屬性的含義如下: 1. root-path 從SD卡開始找 例如 storage/emulated/0/Android/data/com.guadou.kt_demo/cache/pos/naixiao-1122.jpg 2. external-path 從外接SD卡開始 例如 Android/data/com.guadou.kt_demo/cache/pos/naixiao-1122.jpg 3. external-files-path 外接沙盒file目錄 例如 pos/naixiao-1122.jpg (真實目錄在 Android/data/com.guadou.kt_demo/cache/pos/) 4. external-cache-path 外接沙盒cache目錄 例如 naixiao-1122.jpg (真實目錄在 Android/data/com.guadou.kt_demo/cache/) 5. files-path 和上面的同理,只是在內建的data/data目錄下面 6. cache-path 和上面的同理,只是在內建的data/data目錄下面

總共使用的就這麼幾個,大家可以看到我的定義,它是可以重複定義的。

比我我用到的這兩個,是的同樣型別的可以定義多個,

xml <external-cache-path name="external_app_cache" path="." /> <external-cache-path name="naixiao_img" path="pos" />

如果我定義了兩個同類型的 external-cache-path ,他們的 name 你可以隨便取,叫abc都行,主要是path , 推薦大家如果想暴露根目錄就使用點. , 如果想暴露指定的目錄就寫對應的資料夾名稱。

比我我現在有一個圖片在這個目錄下

storage/emulated/0/Android/data/com.guadou.kt_demo/cache/pos/naixiao-1122.jpg

通過 FileProvider 獲取Uri 也是分優先順序的。

比如我定義了pos的目錄,那麼列印如下:

列印Uri:content://com.guadou.kt_demo.fileprovider/naixiao_img/naixiao-1122.jpg

那我們現在把pos的去掉,只要這個。

xml <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卡的規則。

xml <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卡的規則中去了。

那我們再去掉外接卡的規則。此時定義如下

xml <root-path name="myroot" path="." />

此時列印如下:

列印Uri:content://com.guadou.kt_demo.fileprovider/myroot/storage/emulated/0/Android/data/com.guadou.kt_demo/cache/pos/naixiao-1122.jpg

可以看到它的匹配規則是一層一層往上找的,那我們再去掉SD卡的規則呢。。。

那不就空了嗎,此時就崩潰報錯了,這樣是真拿不到Uri了...

使用示例:

說到這裡,我們還沒有真的使用 FileProvider ,下面我們以一個圖片例項為例子演示如何傳送到系統的App

```kotlin //測試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")

    //到系統中找開啟對應的檔案
    openFile(filePath, uri)
}

private fun openFile(path: String, uri: Uri) {
    //取得副檔名
    val extension: String = path.substring(path.lastIndexOf(".") + 1)

    //通過副檔名找到mimeType
    val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
    YYLogUtils.w("mimeType: $mimeType")

    try {
        //構造Intent,啟動意圖,交由系統處理
        startActivity(Intent().apply {
            //臨時賦予讀寫許可權
            addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
            //表示用其它應用開啟
            action = Intent.ACTION_VIEW
            //給Intent 賦值
            setDataAndType(uri, mimeType)
        })
    } catch (e: Exception) {
        e.printStackTrace()
        YYLogUtils.e("不能開啟這種型別的檔案")
    }
}

```

很簡單的一個例子,我們把drawable中的一個圖片,儲存到我們私有沙盒目錄中,目錄為

檔案原始路徑:/storage/emulated/0/Android/data/com.guadou.kt_demo/cache/pos/naixiao-1122.jpg

我們通過 FileProvider 拿到 content://開頭的uri路徑。然後通過Intent匹配找到對於的第三方App來接收。

執行結果如下:

打開了系統自帶的圖片檢視器,還能編輯圖片,檢視資訊等。

那麼列印就如下:

列印Uri:content://com.guadou.kt_demo.fileprovider/external_app_cache/pos/naixiao-1122.jpg

content 是 scheme。

com.guadou.kt_demo.fileprovider 即為我們在清單檔案中定義的 authorities,即是我們的FileProvider的唯一表示,在接收的時候作為host。

這樣封裝之後,當其他的App收到這個Uri就無法從這些資訊得知我們的檔案的真實路徑,相對有安全保障。

其他場景中,比如沙盒中的Apk檔案想要安裝,也是一樣的流程,我們需要賦予讀寫許可權,然後設定DataAndType即可。程式碼的註釋很詳細,大家可以參考參考。

此時我們都是傳送了一個Intent,讓系統自己去匹配符合條件的Activity。那有沒有可能我們自己做一個App去匹配它。

這... 好像還真行。

二、能不能自定義接收檔案?

其實我們仿造系統的App的做法,我們在自定義的Activity中加入指定Filter即可,比如這裡我需要接收圖片,那麼我定義如下的 intent-filter :

```xml

            <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上指明,它可以接收圖片資料,此時我們再回到第一個App,傳送圖片,看看執行的效果:

之前還是圖片檢視器,現在可以選擇我們自己的App來接收圖片資料了,但是我們如何接收資料呢?

其實都是一些固定的程式碼,主要是拿到input流,然後操作流的處理。

```kotlin 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)
                }

            }
        }
    }
}

```

最簡單的做法,直接根據uri開啟輸入流,然後我們可以通過 BitmapFactory 就可以拿到 Bitmap了,就能展示圖片到ImageView上面。

效果如圖:

甚至我們拿到了 input 流,我們還能對流進行copy 操作,把你的圖片儲存到我自己的沙盒目錄中,例如:

```kotlin

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)

            }
        }
    }
}

```

儲存到自己的沙盒檔案之後,我們看一看效果:

好像還真的能行,秀啊。

那此時有人還會有一個疑問,你這方法都是我主動的傳送給別人去展示,去操作!這都不是事,關鍵是能不能讓別人主動的來操作、玩弄我的沙盒檔案?

比如我做的App想獲取微信,支付寶這些別人的App的沙盒中的圖片?行不行?有沒有方法可以做到?

這...,你別逗我了。

三、能不能主動查詢對方的沙盒?

轉頭一想,好像還真行,有操作空間啊... 既然 FileProvider 是繼承自 ContentProvider 。那憑什麼我們的App都能獲取到別人App的資料庫了,不能獲取別人的沙盒檔案呢?那資料庫檔案不也存在沙盒中麼?

例如聯絡人App,我們開發的第三方App可以通過 ContentProvider 獲取到聯絡人App中的聯絡人資料,那麼只要第三方的App定義好對應的 ContentProvider 我不就能獲取到它沙盒的檔案了嗎?

說到就做,我們先把FileProvider設定為可訪問

```xml

    </meta-data>
</provider>

```

是的,android:exported="true" 設定成功之後我們直接通過 contentResolver 去查詢不就好了嗎?

先執行一下試試! 執行就崩了?

什麼鬼哦,看看FileProvider的程式碼,原來不允許開放

原來 FileProvider的 exported 和 grantUriPermissions 都是指定的寫法,不能改變,並且不允許暴露,不允許給別的App主動訪問!

這和我們的需求不符合啊,我就要主動訪問,既然你不行,那我不用你行了吧!我繼承 ContentProvider 行了吧!我自己實現檔案獲取、Cursor封裝行了吧!

不皮了,其實我們直接通過繼承 ContentProvider 並且允許 exported ,然後我們通過自己實現的query方法,返回指定的Cursor資訊,就可以實現!

部分程式碼如下:

```java 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實現    
        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;
    }

}

} ```

我簡單的做了檔案和資料夾的處理,並不完整,如果是檔案我們可以直接返回一個簡單的cursor,如果是資料夾需要大家自己拼接子檔案的cursor並返回。

接下來我們看看其他App如何主動這些檔案,在另一個App中我們先加上許可權: ```xml

<queries>
    <provider android:authorities="com.guadou.kt_demo.fileprovider" />
</queries>

...


```

然後我們直接使用 contentResolver.query

```kotlin 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查詢到檔案的一些基本資訊。具體是哪些資訊,需要對方提供和定義。

如果想操作對方的檔案,由於我們已經拿到了對方的Uri,我們可以直接通過inputStream來操作,例如:

```kotlin 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("儲存檔案成功")

    }

```

這些都是簡單的基本操作,重點是如果我不知道具體的檔案呢?

我就想把對方App的沙盒中的資料夾下面的全部檔案都拿到,行不行?

行!只要對方App配合就行,例如:

```kotlin 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目錄下的全部檔案拿到手,當然了,這個需要對方App封裝對應的cursor才行哦。

列印的Log如下:

只要對方封裝的Cursor,我們可以把名字,大小,uri等資訊都封裝到Cursor中,提供給對方獲取。

總結

FileProvider的主要應用場景就是分享,把自己沙盒中的檔案分享,主動提供給其他匹配的App去使用。

使用其他App的圖片?查詢了目前市場上的主流App,微信,支付寶,閒魚,美團,等App,例如在儲存檔案的時候都沒有存在自己的沙盒中了,都是預設在DCIM或Pictures中,並存入 MediaStore 儲存到相簿中。

這樣就算公共目錄,無需FileProvider,大家直接通過 MediaStore 就能獲取和使用。

而如果想主動訪問其他App的沙盒檔案,則需要對方App全方位配合,一般用於自家App的全家桶之類的應用。相對來說相對應用場景比較少。

不是做不到,只是大家覺得沒有必要而已,畢竟定義和使用相對複雜,並且有暴露風險,被攻擊的風險等。

本文全部程式碼均以開源,原始碼在此。大家可以點個Star關注一波。

好了,本期內容如有錯漏的地方,希望同學們可以指出交流。如果有更好的方法,也歡迎大家評論區討論。

如果感覺本文對你有一點點點的啟發,還望你能點贊支援一下,你的支援是我最大的動力。

Ok,這一期就此完結。

「其他文章」