Android 7.0 文件權限的變化
爲了提高私有文件的安全性,在targetSdk版本爲N或者以後版本的app中,其私有目錄將會限制訪問。這可以防止私有文件元數據的泄露,比如文件大小或者是文件是否存在。但這給開發者帶來了一些不利的影響:
-
文件的所有者不能放寬文件權限,如果你使用MODE_WORLD_READABLE
或者 MODE_WORLD_WRITEABLE操作文件,將會觸發SecurityException
。 -
當跨package域傳遞file://的URI時,接收者得到的將是一個無權訪問的路徑,因此,這將會觸發
FileUriExposedException
。對於這類操作,可以使用ContentProvider
, 但官方推薦的方式是使用FileProvider
.
在targetSdk爲Android N之前的系統版本中,可以使用如下方法調用系統相機拍照並存入指定路徑中
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
Uri uri = Uri.fromFile(sdcardTempFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
但是當你將targetSdk設置爲Android N時 , 在執行到這段代碼時app就crash了,crash的原因便是FileUriExposedException 。這裏有兩種解決方案:
方案1: ContentProvider
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
ContentValues contentValues = new ContentValues(1);
contentValues.put(MediaStore.Images.Media.DATA, sdcardTempFile.getAbsolutePath());
Uri uri = context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
方案2: FileProvider
FileProvider簡介
FileProvider
是 ContentProvider
一個特殊的子類,用於通過創建content://
類型的URI來替代file://
類型的URI,從而爲一個app提供更加安全的文件分享操作.
一個content URI允許你授予臨時的讀寫權限,當你創建一個包含content URI的intent時,爲了將這個content URI發送給一個客戶端app,還可以調用Intent.setFlags()方法添加權限.這個Content類型的URI只要app存在活躍的activity就會一直有效,一旦退出app,該URI失效.
相比之下,File://
類型的URI一旦提供了以後,任何app都可以使用該URI,並且在主動改變URI路徑之前,這個URI一直有效,可以隨時訪問.這使得安全性大爲降低.
由於Content URI提供的更高等級的文件安全機制,使得FileProvider成爲Android安全架構的一個關鍵部分.
FileProvider主要包含以下5方面的知識點:
- 定義一個FileProvider
- 指定可訪問的文件
- 爲一個文件創建一個Content URI
- 爲URI提供臨時權限
- 將Content URI提供給另外一個app
1.定義一個FileProvider
由於FileProvider默認提供了爲文件創建content URI的功能,因此你就不必再在代碼中定義一個它的子類了.你可以直接在XML文件中聲明一個FileProvider.
聲明FileProvider步驟:
- 在application標籤下添加一個標籤
- 設置android:name 屬性爲android.support.v4.content.FileProvider
- 基於app的包名來設置android:authorities屬性,例如:包名爲mydomain.com,那麼授權路徑爲:com.mydomain.fileprovider
- 設置android:exported 屬性爲false;FileProvider不需要public
- 設置android:grantURIPermissions 屬性爲true,允許文件的臨時訪問.
<manifest>
...
<application>
...
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.mydomain.fileprovider"
android:exported="false"
android:grantURIPermissions="true">
...
</provider>
...
</application>
</manifest>
如果想重寫FileProvider中的方法,那麼繼承FileProvider類,並且在XML文件中的聲明時,android:name 需要使用自定義類的全路徑類名
2.指定可訪問的文件
一個FileProvider只能爲提前指定好的文件目錄生成content URI.可以通過在xml文件中,以標籤的形式指定文件目錄.
比如下面的代碼,表明你計劃爲 images/ 目錄下的子文件請求content URI
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="my_images" path="images/"/>
...
</paths>
<paths>
標籤必須包含一個或多個下列標籤
<files-path name="name" path="path" />
內部存儲路徑,與Context.getFilesDir()返回的路徑一致<cache-path name="name" path="path" />
內部緩存路徑,與Context.getExternalFilesDir() 返回的路徑一致<external-path name="name" path="path" />
外置存儲卡根目錄,與Context.getExternalFilesDir()返回的路徑一致
注意
- name代表URI的路徑,爲了安全起見,隱藏了具體的目錄位置 , 具體的目錄位置由path字段指定
- 所有的path指定的都是目錄名,包含了旗下的子目錄,而不是文件名.無法通過文件名來指定單個文件,也無法通過通配符的形式指定一系列子文件.
必須爲每個需要content URI的路徑在xml提供標籤來指定,比如下面的代碼就提供了兩個目錄
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="my_images" path="images/"/>
<files-path name="my_docs" path="docs/"/>
</paths>
在資源目錄下創建對應的xml文件,比如res/xml/file_paths.xml,在Manifest文件中將路徑xml通過<meta-data>
標籤與FileProvider
綁定起來.
<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/file_paths" />
</provider>
3.爲文件生成content URI
爲了與其他app通過content URI來分享文件,你的app需要生成一個content URI.方法如下:
分享方app:
- 爲該文件創建一個File對象
- 將File對象傳遞給getUriForFile(),獲取一個URI對象
- 將該URI對象通過intent傳遞給其他的app
接收方app:
通過ContentResolver.openFileDescriptor獲取一個ParcelFileDescriptor , 讀取該文件
例如:
假設你的app需要提供給其他app一個FileProvider , authority授權名爲com.mydomain.fileprovider , 爲內部存儲目錄images/
目錄下的default_image.jpg
文件創建一個content URI.
File imagePath = new File(Context.getFilesDir(), "images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = FileProvider.getUriForFile(getContext(), "com.mydomain.fileprovider", newFile);
代碼結果:
getUriForFile()
方法返回了一個contentUri , 路徑內容爲: content://com.mydomain.fileprovider/my_images/default_image.jpg
4.給URI提供臨時權限
從getUriForFile()獲取到content URI以後,通過以下任意一種方式授予訪問權限
方式1:
調用Context.grantUriPermission(package, Uri, mode_flags)
爲content URI 授權.使用指定的mode_flags
這裏需指定授權的包名和mode_flags,權限分爲
- FLAG_GRANT_READ_URI_PERMISSION 讀
- FLAG_GRANT_WRITE_URI_PERMISSION 寫
可單選可多選
權限的有效期爲:手動撤銷授權revokeUriPermission() 或 重啓設備.
方式2:
通過調用Intent
的 setData()
方法將此content URI放入intent中
調用Intent.setFlags()
,選項爲
FLAG_GRANT_READ_URI_PERMISSION 讀
FLAG_GRANT_WRITE_URI_PERMISSION 寫
可單選可多選
將此intent發送給其他app.一般情況下,會通過setResult()
方法發送給其他intent
權限有效期 : 當接收到的activity處於活躍狀態時持續有效 , 退出時自動失效,一個activity獲取到得content URI權限,這個權限會延展至所屬的整個app.
5.爲其他app提供content URI
5.1其他app請求自己app
爲一個文件提供content URI給其他app有很多形式,其中一個常用的方式時接收其他app通過startActivityResult()
方法啓動自己的app , 通過Intent來啓動自己app中的一個Activity.
你可以立即返回一個content URI , 或者展示一個交互界面供用戶選擇一個文件 , 一旦用戶選擇了該文件 , 就將該文件的的content URI返回給請求者app . 無論哪種方式 , 最終都通過setResult()
方式將content URI返回給請求者.
5.2自己app請求其他app
將content URI放入ClipData
對象中,然後將ClipData對象添加進Intent中,再將Intent發送給一個app.
調用Intent.setClipData()
來添加ClipData
對象,可以放入1個或多個 . 每個ClipData對象都包含一個content URI
當通過Intent.setFlags()
來設置臨時訪問權限時,這些權限會適用於所有的content URIs
注意:
Intent.setClipData()
方法只能在API 16(Android4.1)以上才能使用 , 如果爲了確保版本的兼容性,那麼只能每次通過intent發送一個content URI.將ACTION_SEND
添加進action
,通過setData()
將content URI添加進data
.
FileProvider支持的path類型
從FileProvider源碼查看其中涉及的Path類型
private static final String TAG_ROOT_PATH = "root-path";
private static final String TAG_FILES_PATH = "files-path";
private static final String TAG_CACHE_PATH = "cache-path";
private static final String TAG_EXTERNAL = "external-path";
private static final String TAG_EXTERNAL_FILES = "external-files-path";
private static final String TAG_EXTERNAL_CACHE = "external-cache-path";
從Android官方文檔上可以看出FileProvider提供以下幾種path類型:
<files-path path="" name="camera_photos" />
該方式提供在應用的內部存儲區的文件/子目錄的文件。它對應Context.getFilesDir()
返回的路徑,例如/data/data/com.crocutax.mytest/files
<cache-path name="name" path="path" />
該方式提供在應用的內部存儲區的緩存子目錄的文件。它對應Context.getCacheDir()
返回的路徑,例如/data/data/com.crocutax.mytest/cache
<external-path name="name" path="path" />
該方式提供在外部存儲區域根目錄下的文件。它對應Environment.getExternalStorageDirectory()
返回的路徑,例如/storage/emulated/0
<external-files-path name="name" path="path" />
該方式提供在應用的外部存儲區根目錄的下的文件。它對應Context.getExternalFilesDir(String type)
返回的路徑。例如
ContextCompat.getExternalFilesDirs(MainActivity.this,null)[0]:
<external-cache-path name="name" path="path" />
該方式提供在應用的外部緩存區根目錄的文件。它對應Context.getExternalCacheDir()
返回的路徑。
ContextCompat.getExternalCacheDirs(MainActivity.this)[0]:
FileProvider的使用示例
1.在Manifest文件中定義FileProvider
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.touchmedia.daolan.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
2.res/xml/file_paths中指定共享目錄
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="sdcard_path" path="" />
</paths>
3.通過FileProvider獲取ContentUri
//安裝app
...
//通過FileProvider獲取contentUri
Uri contentUri = FileProvider.getUriForFile(mContext, "com.sadaharusong.fileprovider", apkFile);
//授予臨時訪問權限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(contentUri,"application/vnd.android.package-archive");
//跳往安裝界面
mContext.startActivityForResult(intent,INSTALL_APP);
其他涉及到本地文件讀取的操作,例如圖庫,操作方式都一樣,跟以前唯一的不同僅僅只是FileProvider的引入.