上篇文章瞭解到應用級檔案只能被其所建立的應用程式所訪問,那麼其他應用程式是不是就無論如何都無法訪問了呢?肯定不是的,只要檔案經過其建立的應用程式授權,還是可以被其他應用程式所訪問的。這也就是應用級檔案的共享。

系統只允許共享包含實際資料的純檔案型別,而不推薦共享包含檔案的目錄型別。

對於檔案的訪問可以使用java.io.File系統檔案類,但是如果想將該檔案分享出去,則需要藉助android.net.Uri路徑定位符類。Uri類是Android系統下自定義的路徑定位符規則,其符合Java中定義的java.net.URI標準資源定位符規範,並新增了getPathSegments()獲取分割路徑列表的方法和getQueryParameter(String key)獲取指定的額外引數方法等。

通常思路是將要分享的File檔案物件轉換成特定的Uri路徑定位符物件。在Android7即API 24版本及以後的版本中,系統對Uri增加了授權處理,這將在後文詳細解釋。

之後將該Uri物件可以作android.os.Bundle資料物件通過android.content.Intent意圖類傳遞。

最終在接收到意圖的應用程式中,可以收到Uri路徑並對其做不同處理,即可訪問該路徑下的檔案,針對Uri中的內容不同,一般需要做不同的轉換處理,這將在後文詳細解釋。

檔案分享方

目標版本級別小於24的應用程式

在Android7即API 24版本以前,應用程式內的任何File物件,都可以通過靜態方法Uri.fromFile(File file)將引數 file 直接轉換為Uri物件。

得到的Uri物件可直接操作賦值給Intent意圖物件的setData(Uri data)方法中的引數 data ,或putExtra(String key, Parcelable value)方法中的引數 value,從而分享傳遞出去。

目標版本級別大於等於24的應用程式

從Android7即API 24版本開始,應用程式內部共享檔案,仍然可以使用Uri.fromFile(File file)方法,但是如果想在應用程式中對得到的Uri物件直接傳遞給其他應用程式使用,會丟擲android.os.FileUriExposedException異常。因此,在使用androidx依賴包的應用程式中可以藉助androidx.core.content.FileProvider檔案分享提供類,在使用support-v4依賴包的應用程式中可以藉助android.support.v4.content.FileProvider檔案分享提供類,獲取相關Uri物件,併為其授權,之後才可在不同應用程式間共享訪問。

對於FileProvider的使用,要先做準備工作。

首先需要在應用程式的清單檔案中註冊。在<application></application>標籤中增加<provider></provider>標籤。

在該標籤中指定屬性android:name="androidx.core.content.FileProvider"以繫結檔案分享提供類;

另外在該標籤中指定屬性android:authorities="xxx",這裡的屬性值xxx是應用程式所在系統內唯一的,其定義規則通常建議以應用程式包名為字首的域名,且在後文程式碼中有使用;

同時在改標籤中指定屬性android:grantUriPermissions="true",屬性值為 true 時,表示允許對得到的Uri所定位的File檔案授予臨時讀寫許可權。

最後在改標籤中增加<meta-data/>標籤,以指定所使用的資料資訊。在該資料標籤中,必須定義其屬性為android:name="android.support.FILE_PROVIDER_PATHS",同時定義屬性android:resource="@xml/file_paths" ,這裡的屬性值@xml/file_paths是指向定義的資原始檔file_paths.xml

在自定義的資源目錄 res 下建立子目錄 xml,該目錄中建立檔案 file_paths.xml 。定義<paths></paths>根標籤以指定要分享的檔案路徑。在根標籤中可以根據要分享的檔案所屬路徑不同而使用不同型別的標籤名作為子標籤插入,子標籤中包含有namepath兩個屬性,分別設定該路徑的別名和子目錄。其中子標籤可用型別與程式碼中獲取路徑的對應關係可參考下表。

程式碼獲取路徑 子標籤名
context.getFilesDir() <files-path>
context.getCacheDir() <cache-path>
Environment.getExternalStorageDirectory() <external-path>
context.getExternalMediaDirs() <external-media-path>
context.getExternalFilesDir() <external-file-path>
context.getExternalCacheDir() <external-cache-path>

在清單檔案中註冊FileProvider之後,就可以在程式碼獲取Uri的地方,與低版本中獲取Uri的方法Uri.fromFile(File file)所不同的是,替換為FileProvider的靜態方法getUriForFile(Context context, String authority, File file)。引數 context 是當前應用程式所在的上下文環境;引數 authority 則是在清單檔案中註冊FileProvider時,屬性android:authorities所表示的值;引數 file 依然是應用程式中要分享的檔案物件。

在使用FileProvider.getUriForFile(Context context, String authority, File file)獲取到Uri之後,需要授權給要使用該路徑的應用程式,在能獲取到上下文環境Context物件的地方,呼叫grantUriPermission (String toPackage, Uri uri, int modeFlags)方法授權。引數 toPackage 即為要授權使用該路徑的應用程式所在包名;引數 uri 是上文獲取到的路徑定位符物件;引數 modeFlags 是用來表示許可權型別的標誌位,其值目前包括四種:Intent.FLAG_GRANT_READ_URI_PERMISSION=1的讀許可權,Intent.FLAG_GRANT_WRITE_URI_PERMISSION=2的寫許可權,Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION=4表示長久授權,和Intent.FLAG_GRANT_PREFIX_URI_PERMISSION=8對匹配引數 uri 的字首的所有路徑定位符均授權。

在對Uri物件授權之後,也就可以像老版本一樣將其封裝到Intent意圖物件中傳送給其他應用了。

檔案接收方

一般地,在分享檔案的接收方,如果只是對接收到的檔案內容做讀寫處理,可以藉助java.io.FileDescriptor檔案描述類,使用官方推薦的簡單方式操作。

在接收方收到的Intent意圖物件呼叫getData()方法,得到要接收的檔案所對應的Uri路徑定位符物件。

在可以使用上下文環境Context物件的位置,呼叫其getContentResolver()方法,獲取上下文環境中的ContentResolver內容解析類物件。

藉助ContentResolver物件的openFileDescriptor (Uri uri, String mode)方法,返回android.os.ParcelFileDescriptor系統封裝的檔案描述類物件。其中引數 uri 就是上文接收到的路徑定位符物件;引數 mode 則標記了檔案的開啟方式,包括只讀許可權"r",只寫許可權"w",追加寫入許可權"wa",讀寫許可權"rw"等。

在得到ParcelFileDescriptor物件後,呼叫其getFileDescriptor()方法,可以獲得FileDescriptor檔案描述物件。

最終通過得到的檔案描述物件,建立FileInputStream(FileDescriptor fdObj)構造方法或FileOutputStream(FileDescriptor fdObj)構造方法獲取檔案輸入流物件或檔案輸出流物件,以此操作檔案內容。

還有些特殊需求,對於分享檔案接收方來說,是要對收到的檔案獲取其所在目錄並做進一步操作,也就是需要對得到的Uri路徑定位符物件轉換為File檔案物件。這方面官方並沒有像上文那樣簡單統一的呼叫方式,而需要開發者針對不同的版本做單獨處理。

目標版本級別小於29的應用程式

在Android10即API 29之前的版本中,網上有一堆程式碼可參考,其思路就是查詢系統資料庫中不同媒體檔案是否與Uri物件所指向位置匹配,進而確定該Uri物件指向的媒體檔案目錄,在應用程式內藉助上下文環境獲取其媒體檔案所在目錄File物件,從而匹配要查詢的Uri路徑定位符物件。

目標版本級別大於等於29的應用程式

從Android10及API 29版本開始,由於應用程式僅可訪問自己分割槽儲存內部的檔案,對於收到的分享檔案路徑定位符Uri物件,也只能先使用上文官方推薦的讀取檔案方式,將Uri物件所指向的檔案先儲存一份到自己應用程式內部的私有目錄下,再將這份私有目錄下的儲存檔案轉為File物件使用。