FileProvider 在 Android N 上的应用
作者:才華橫溢的段老師?藍田大營
一、背景
Android 從 N 開始不允許以 file:// 的方式通過 Intent 在兩個 App 之間分享文件,取而代之的是通過 FileProvider 生成 content://Uri 。如果在 Android N 以上的版本繼續使用 file:// 的方式分享文件,則系統會直接拋出異常,導致 App 出現 Crash ,同時會報以下錯誤日志:
FATAL EXCEPTION: mainProcess: com.inthecheesefactory.lab.intent_fileprovider, PID: 28905android.os.FileUriExposedException: file:///storage/emulated/0/.../xxx/xxx.jpg exposed beyond app through ClipData.Item.getUri()at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)at android.net.Uri.checkFileUriExposed(Uri.java:2346)at android.content.ClipData.prepareToLeaveProcess(ClipData.java:832)當然如果工程的 targetSDK 小于24,暫時還不會遇到這個問題,一旦升級到24及以上,則會立即出現上述問題,所以提早做好預防很有必要,否則等到線上曝出大量的 bug 就很被動了。
二、關于 FileProvider
官方對于 FileProvider 的解釋為:FileProvider 是一個特殊的 ContentProvider 子類,通過 content://Uri 代替 file://Uri 實現不同 App 間的文件安全共享。
當通過包含 Content URI 的 Intent 共享文件時,需要申請臨時的讀寫權限,可以通過 Intent.setFlags() 方法實現。
而 file://Uri 方式需要申請長期有效的文件讀寫權限,直到這個權限被手動改變為止,這是極其不安全的做法。因此 Android 從 N 版本開始禁止通過 file://Uri 在不同 App 之間共享文件。
三、FileProvider 的使用流程
完成整個文件共享的流程,需要配置以下5點:
1. 定義 FileProvider
FileProvider 已經把文件生成 Content URI 的工作幫我們做掉了,因此我們只需要在 AndroidManifest.xml 文件中配置 <provider> 元素并提供相應的屬性。
重要的屬性包括以下四個:
- 設置 android:name 為android.support.v4.content.FileProvider,這是固定的,不需要手動更改;
- 設置 android:authorities 為 application id + .provider ;
- 設置 android:exported 為 false ,表示 FileProvider 不是公開的;
- 設置 android:grantUriPermissions 為 true 表示允許臨時讀寫文件。
此處需要特別說明的是
以下是一個簡單的示例:
<manifest>...<application>...<providerandroid:name="android.support.v4.content.FileProvider"android:authorities="${applicationId}.provider"android:exported="false"android:grantUriPermissions="true">...</provider>...</application> </manifest>需要說明的是 ${applicationId} 是占位符,Gradle 會替換成我們在 build.gralde 中定義的 applicationId "com.domain.example",如果 build.gradle 文件中沒有定義,那么 application id的默認值是 App 的 package name。
2. 指定有效的文件
在生成 Content URI 之前你還需要提前指定文件目錄,通常的做法是在 res 目錄下新建一個 xml 文件夾,然后創建一個 xml 文件,在此文件中指定共享文件的路徑和名字,示例如下:
<paths xmlns:android="http://schemas.android.com/apk/res/android"><external-path name="my_images" path="images/"/>... </paths>其中 name 屬性和 path 屬性必填, name 表示共享文件的名字, path 代表文件路徑。
- external-path 代表文件位于手機外部存儲空間,訪問效果如同 Environment.getExternalStorageDirectory();
- files-path 代表文件位于手機內部存儲空間,訪問效果如同 getFilesDir();
- cache-path 代表文件位于手機內部緩存空間,訪問效果如同 getCacheDir()。
xml 文件創建完成后,還需要在 manifest 文件的 <provider> 元素下完成相應的配置,假定 xml 文件命名為 file_paths.xml ,示例如下:
<providerandroid:name="android.support.v4.content.FileProvider"android:authorities="${applicationId}.provider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/file_paths" /> </provider>3. 為共享文件生成 Content URI
文件配置完成后還需要生成可以被其他 App 訪問的 Content URI,可以直接調用 FileProvider 提供的 getUriForFile(File file) 方法,顧名思義,傳入文件名稱就可以得到相應的 Content URI 。需要訪問該文件的 App 可以通過 ContentResolver.openFileDescriptor 得到一個 ParcelFileDescriptor 對象。
假定你想要共享一個圖片文件,文件存放的位置為手機內部存儲空間下的 images 文件夾,圖片文件名字為 default_name.jpg ,那么生成 Content URI 方式如下:
File imagePath = new File(getContext().getFilesDir(), "images"); File newFile = new File(imagePath, "default_image.jpg"); Uri contentUri = getUriForFile(getContext(), "com.mydomain.provider", newFile);最后生成的 Content URI 為
content://com.domain.example.provider/images/default_image.jpg.4. 申請臨時讀寫文件權限
上文已經提到 FileProvider 可以申請臨時讀寫文件權限,以增強安全性,所以 Content URI 生成完成后,還需要申請臨時訪問權限。
通常直接通過 intent.setFlags 即可完成,具體的權限名稱為:Intent.FLAG_GRANT_READ_URI_PERMISSION 和 Intent.FLAG_GRANT_WRITE_URI_PERMISSION。
5. 發送 Content URI 至其他的 App
萬事已備,只需要發送出去即可,通常都會使用 startActivityForResult 方法發送,可以在 onActivityResult 中獲取其他 App 的處理結果,完成整個操作閉環。
三、實用場景——手機照相
在 Android N 之前的版本調用相機獲取圖片可以用如下代碼實現:
// 設置照片需要存儲的位置 photoPath = FileUtil.getImageFile().getPath() Intent intent = new Intent();// 指定開啟系統相機的Action intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE); intent.addCategory(Intent.CATEGORY_DEFAULT);// 把文件地址轉換成Uri格式 Uri uri = Uri.parse("file://" + photoPath); intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); activity.startActivityForResult(intent, requestCode);如果要想在 Android N 及以上版本上不會出錯,則必須將 file:// 形式替換成 content:// ,具體的代碼如下:
Intent intent = new Intent(); intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);// 系統版本大于N的統一用FileProvider處理 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {// 將文件轉換成content://Uri的形式Uri photoURI = FileProvider.getUriForFile(activity,activity.getPackageName()+ ".provider",new File(photoPath));// 申請臨時訪問權限intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI); } else {intent.addCategory(Intent.CATEGORY_DEFAULT);Uri uri = Uri.parse("file://" + photoPath);intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); } activity.startActivityForResult(intent, requestCode);需要注意的是 getPackageName() 返回值是 application id,關于 application id 上文已經解釋過,此處不再重復。
實用場景——微信朋友圈多圖分享
微信官方不支持朋友圈直接多圖分享,Android 之前的版本由于沒有強制限制 file:// 的使用,所以可以通過訪問微信包名的方式實現朋友圈多圖分享,但是Android N 之后這種“曲線救國”的方式就不行了。
先來看一下之前如何通過訪問包名實現朋友圈多圖分享,代碼如下:
Intent intent = new Intent(); intent.setComponent(new ComponentName("com.tencent.mm", "com.tencent.mm.ui.tools.ShareToTimeLineUI")); intent.setAction("android.intent.action.SEND_MULTIPLE");// List存儲多張圖片地址 ArrayList<Uri> localArrayList = new ArrayList<>(); for (int i = 0, size = localPicsList.size(); i < size; i++) {localArrayList.add(Uri.parse("file:///" + localPicsList.get(i))); }intent.putParcelableArrayListExtra("android.intent.extra.STREAM", localArrayList); intent.setType("image/*"); intent.putExtra("Kdescription", desc); context.startActivity(intent);這種方式可以直接繞過微信官方 SDK 實現多圖分享,無需手動選擇圖片,唯一的問題就是沒有分享結果的回調,也就是說無法判斷是否分享成功,這在大部分情況下依然是一種可以接受的方案。
但是如果 targetSDK 大于等于24,那么這項功能就無效了,原因就是 Android N 不允許 file://Uri 的方式在不同的 App 間共享文件,但是如果換成 FileProvider 的方式,經試驗發現依然是無效的,所以在 Android N 上無法實現朋友圈直接多圖分享。
原文地址: https://zhuanlan.zhihu.com/p/26139355總結
以上是生活随笔為你收集整理的FileProvider 在 Android N 上的应用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android 插件技术实战总结
- 下一篇: Activity到底是什么时候显示到屏幕