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
.
FileProvider
是ContentProvider
的子類,它通過為一個文件創建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...(需要梯子)