FileProvider使用詳解(拍照、安裝APP、共享文件)


FileProvider

在Android7.0及之后我們無法直接將一個FileUri共享給另一個程序進行使用。系統會拋出一個異常FileUriExposedException。官方是這樣描述的:

The exception that is thrown when an application exposes a file:// Uri to another app.

當一個應用程序暴漏一個file:// Uri給另一個app時就會拋出這個異常。

This exposure is discouraged since the receiving app may not have access to the shared path. For example, the receiving app may not have requested the Manifest.permission.READ_EXTERNAL_STORAGE runtime permission, or the platform may be sharing the Uri across user profile boundaries.

由於需要接收fileURI的應用程序可能無法訪問共享的路徑,因此不建議這樣做。這可能是由於使用了Manifest.permission.READ_EXTERNAL_STORAGE權限導致,或者平台可以跨越用戶配置邊界共享Uri。

PS:這個很好理解,比如說我有一個app被裝在了手機上,但是沒有申請READ_EXTERNAL_STORAGE權限(6.0后需要動態申請),但是我在另一個程序中請求這個app來讀取這個文件是不是就會出現問題了,肯定就會出現異常了。所以說使用了內容提供程序,數據的讀取是由內容提供者進行讀取的,這樣就要求數據提供者必須具有這個權限,也保證了數據安全。

Instead, apps should use content:// Uris so the platform can extend temporary permission for the receiving app to access the resource.

我們應該使用content:// Uris對其進行替換,以便平台可以為需要訪問特定資源的app擴展臨時權限。

This is only thrown for applications targeting Build.VERSION_CODES#N or higher. Applications targeting earlier SDK versions are allowed to share file:// Uri, but it's strongly discouraged.

這個異常只會在目標版本大於等於7.0時拋出。之前的版本可以繼續使用fileURI,不過不推薦這樣做。

這些都是由於7.0開啟了嚴格模式(StrictMode)造成的,官方在7.0的變更中是這么說的:

對於面向 Android 7.0 的應用,Android 框架執行的 StrictMode API 政策禁止在您的應用外部公開 file:// URI。如果一項包含文件 URI 的 intent 離開您的應用,則應用出現故障,並出現 FileUriExposedException 異常。

FileProvider類的繼承關系

java.lang.Object android.content.ContentProvider android.support.v4.content.FileProvider

官方介紹

FileProvider is a special subclass of ContentProvider that facilitates secure sharing of files associated with an app by creating a content:// Uri for a file instead of a file:/// Uri.

FileProviderContentProvider的子類,它通過為一個文件創建content:// Uri 來替換file:/// Uri,以此來達到文件的安全共享。

核心步驟

1、定義FileProvider

2、定義可用的文件路徑

3、為定義的FileProvider添加文件路徑

4、為特定文件生成ContentURI

5、授予ContentURI授予臨時權限

1、定義FileProvider

由於FileProvider提供了ContentURI的生成方法,所以我們無需在代碼中定義寫一個它的子類。以下代碼中的name屬性是固定的,authorities可以自己定義,一般是包名字加上.fileprovider。exported設置為false,因為通常是拒絕外部直接訪問的。grantUriPermissions需要為true,需要授予臨時的Uri權限。

<manifest> ... <application> ... <provider android:name="android.support.v4.content.FileProvider" android:authorities="com.mydomain.fileprovider" android:exported="false" android:grantUriPermissions="true"> ... </provider> ... </application> </manifest>

2、定義可用的文件路徑

FileProvider只能為預先指定的目錄中的文件生成可用的ContentURI。要指定目錄,需要使用<paths>

該文件需要建立在res目錄下名為xml的目錄下,xml目錄需要自己建立。

<?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <!--定義APP的存放目錄--> <external-path name="AppInstaller" path="/Download"></external-path> </paths>

paths下可以包含一個或者多個子節點。

<root-path/> 代表設備的根目錄new File("/");//很少用 //app內部存儲 <files-path/> 代表context.getFilesDir() <cache-path/> 代表context.getCacheDir() //sd卡存儲 <external-path/> 代表Environment.getExternalStorageDirectory() <external-files-path>代表context.getExternalFilesDirs() <external-cache-path>代表getExternalCacheDirs()

我們還可以在path中用.代替所有目錄。

3、為定義的FileProvider添加文件路徑

這里我們加入剛才添加的path文件,注意meta-data中的name項必須是android.support.FILE_PROVIDER_PATHS。

<provider android:name="android.support.v4.content.FileProvider" android:authorities="com.mydomain.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/my_path"></meta-data> </provider>

記不住這個name怎么辦?好上頭!!!!懶人總是有辦法。在FileProvider類的內部正好有一個定義可供我們Copy。

private static final String META_DATA_FILE_PROVIDER_PATHS = "android.support.FILE_PROVIDER_PATHS";

4、為特定文件生成ContentURI

FileProvider提供了getUriForFile函數幫助我們生成ContentURI。這里需要注意的是我們使用的文件路徑必須是前邊在path中定義的。否則要path何用....。

第一個參數為context,第二個是定義的provider中設置的authorities,第三個是一個File對象。

//文件路徑 File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getCanonicalPath() + "/apps/MyApp.apk"); //獲取文件對應的content類型Uri Uri uri = FileProvider.getUriForFile(this, "com.mydomain.fileprovider", file);

觀察我們生成的Uri示例,上邊是我們普通的fileUri下邊是我們生成的ContentUri,區別就在於ContentUri沒有暴露具體的文件路徑。

//普通的fileUri(通過Uri.fromFile(file)獲取) file:///storage/emulated/0/Download/apps/MyApp.apk //contentUri content://com.qylost.fileproviderdemo.fileprovider/AppInstaller/MyApp.apk

常見使用場景

1、跨程序共享文件

以下我們通過兩個app演示兩個程序使用FileProvider共享數據。提供數據的被稱為:ServerApp,接受數據的被稱為:ClientApp。

ServerApp:

主要是如上所說的在Manfiest中定義provider,以及定義共享路徑。

<provider android:name="android.support.v4.content.FileProvider" android:authorities="com.qylost.fileproviderdemo.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/my_path"></meta-data> </provider>
<paths xmlns:android="http://schemas.android.com/apk/res/android"> <files-path name="ShareToMyApp" path="."></files-path> </paths>

ClientApp:

這里我們新增了一個Main2Activity,在這里讀取ServerApp通過FileProvider傳來的數據。

public class Main2Activity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main2); Intent intent = getIntent(); if (intent != null && intent.getData() != null) { try { ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(intent.getData(), "r"); FileReader reader = new FileReader(parcelFileDescriptor.getFileDescriptor()); BufferedReader bufferedReader = new BufferedReader(reader); String res = new Scanner(bufferedReader).useDelimiter("\\A").next();//解析傳來的數據 Toast.makeText(this, res, Toast.LENGTH_SHORT).show();//彈出 } catch (FileNotFoundException e) { e.printStackTrace(); } } } }

這里加入intent-filter,定義了action的名稱,以及mimeType,這個在請求的時候需要用到。注意category不可少。

<activity android:name=".Main2Activity"> <intent-filter> <data android:mimeType="share/text" /> <action android:name="com.qylost.fileproviderdatareceverdemo.SHARE"/> <category android:name="android.intent.category.DEFAULT"/> </intent-filter> </activity>

在ServerApp中調用如下代碼,共享數據:

//在files目錄下寫入測試數據 writeTestData();//這里在內部files文件目錄下寫入了文本內容Hello File Provider!文件名為:FileProviderTest.txt //開始共享數據 File file = new File(getFilesDir(), "FileProviderTest.txt"); Uri uri = FileProvider.getUriForFile(this, "com.qylost.fileproviderdemo.fileprovider", file); Intent intent = new Intent("com.qylost.fileproviderdatareceverdemo.SHARE");//這個就是在上邊配置intent-filter時設置的action name intent.setDataAndType(uri, "share/text");//在上邊intent-filter中設置的mimeType intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//授予臨時讀取權限 startActivity(intent);

效果圖:

2、打開App安裝程序

//文件路徑 File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getCanonicalPath() + "/MyApp.apk"); Intent intent = new Intent(Intent.ACTION_VIEW); //獲取文件對應的content類型Uri Uri uri = FileProvider.getUriForFile(this, "com.qylost.fileproviderdemo.fileprovider", file); intent.setDataAndType(uri, "application/vnd.android.package-archive"); //intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//可以不加 intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(intent);

3、拍照

//定義文件名稱 String fileName = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".jpg"; String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getCanonicalPath() + "/" + fileName; //獲取文件的ContentURI File file = new File(path); Uri uri = FileProvider.getUriForFile(this, "com.qylost.fileproviderdemo.fileprovider", file); //定義Intent對象 Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);//設置Action為MediaStore下的ACTION_IMAGE_CAPTURE intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);//設置Extra標志為輸出類型 intent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);//授予臨時權限 startActivityForResult(intent, 1); //接收拍照結果 @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { //拍照成功(這里可以將請求拍照的File對象定義為成員變量,這樣成功后就可以拿到圖片了) if (requestCode == 1 && resultCode == RESULT_OK) { Toast.makeText(this, "Success", Toast.LENGTH_SHORT).show(); } super.onActivityResult(requestCode, resultCode, data); }

基本工作原理

使用fileUri的工作流程圖:

1、A共享文件絕對路徑給B

2、B通過路徑讀取數據

通過fileUri共享文件簡單粗暴,直接將路徑進行共享,這樣做會存在一些問題:

1、文件路徑暴露。

2、這個文件路徑可能是一個外部存儲路徑(外部存儲路徑需要申請權限,可能App B沒有這個權限,就會出現異常。再或者AppA沒有外部存儲讀寫權限,那么將文件讀取交給了一個具有外部存儲讀寫權限的App就會存在安全隱患)。

為了解決這兩個問題,所以使用contentURI,使用“相對“路徑解決路徑暴露問題,數據讀取是交由提供者來完成的。

使用ContentUri的工作流程圖:

A僅僅給B分享了ContentURI,具體的文件讀取是由內容/數據提供方(App A)來完成的,App B只能去問App A拿數據。

1、A共享ContentURI給B

2、B拿着這個URI找A要數據

3、A讀取文件中的數據給B

手動關閉嚴格模式

不推薦這么來搞,不過還是要知道的。

//手動關閉嚴格模式 StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder(); builder.detectAll(); StrictMode.setVmPolicy(builder.build());

參考文獻

1、https://developer.android.com...

2、https://blog.csdn.net/chen_wh...

3、https://blog.csdn.net/Next_Se...

4、https://developer.android.com...(需要梯子)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM