Android 適配之FileProvider的使用

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簡介

FileProviderContentProvider 一個特殊的子類,用於通過創建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步驟:

  1. 在application標籤下添加一個標籤
  2. 設置android:name 屬性爲android.support.v4.content.FileProvider
  3. 基於app的包名來設置android:authorities屬性,例如:包名爲mydomain.com,那麼授權路徑爲:com.mydomain.fileprovider
  4. 設置android:exported 屬性爲false;FileProvider不需要public
  5. 設置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:

  1. 爲該文件創建一個File對象
  2. 將File對象傳遞給getUriForFile(),獲取一個URI對象
  3. 將該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:
通過調用IntentsetData()方法將此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的引入.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章