android SAF儲存訪問框架
Android 4.4(API 級別 19)引入了Storage Access Framework儲存訪問框架 (SAF),SAF 讓使用者能夠在其所有首選文件儲存提供程式中方便地瀏覽並開啟文件、影象以及其他檔案。 使用者可以通過易用的標準 UI,以統一方式在所有應用和提供程式中瀏覽檔案和訪問最近使用的檔案。
雲端儲存服務或本地儲存服務可以通過實現封裝其服務的 DocumentsProvider 參與此生態系統。只需幾行程式碼,便可將需要訪問提供程式文件的客戶端應用與 SAF 整合。
SAF 包括以下內容:
- 文件提供程式:一種內容提供程式,允許儲存服務(如 Google+Drive/">Google Drive)顯示其管理的檔案。 文件提供程式作為 DocumentsProvider 類的子類實現。文件提供程式的架構基於傳統檔案層次結構,但其實際資料儲存方式由您決定。Android 平臺包括若干內建文件提供程式,如 Downloads、Images 和 Videos。
- 客戶端應用:一種自定義應用,它呼叫 ACTION_OPEN_DOCUMENT 和/或 ACTION_CREATE_DOCUMENT Intent 並接收文件提供程式返回的檔案;
- 選取器:一種系統 UI,允許使用者訪問所有滿足客戶端應用搜索條件的文件提供程式內的文件。下面是Google的N6上的選取器

具體的儲存訪問框架流如下圖

SAF 提供的部分功能如下:
- 允許使用者瀏覽所有文件提供程式而不僅僅是單個應用中的內容;
- 讓您的應用獲得對文件提供程式所擁有文件的長期、永續性訪問許可權。 使用者可以通過此訪問許可權新增、編輯、儲存和刪除提供程式上的檔案;
- 支援多個使用者帳戶和臨時根目錄,如只有在插入驅動器後才會出現的 USB 儲存提供程式。
如何使用SAF
在Android 4.3 及更低版本,如果想讓應用從其他應用中檢索檔案,它必須呼叫 ACTION_PICK 或 ACTION_GET_CONTENT 的 Intent;對於 Android 4.4 及更高版本,使用 ACTION_OPEN_DOCUMENT的Intent,這樣會觸發選取器(Picker),緊接著Picker會從所有註冊的Provider中定位符合intent請求條件的資料來源,並且通過統一的介面顯示給使用者,當用戶選擇完檔案/目錄後,Picker會將資料以Uri的形式返回給客戶端,這樣客戶端就拿到這些檔案/目錄的讀寫許可權並進行想要的處理。 需要注意的是ACTION_OPEN_DOCUMENT 並非設計用於替代 ACTION_GET_CONTENT。應使用的 Intent 取決於應用的需要:
- 如果您只想讓應用讀取/匯入資料,請使用 ACTION_GET_CONTENT。使用此方法時,應用會匯入資料(如影象檔案)的副本;
- 如果您想讓應用獲得對文件提供程式所擁有文件的長期、永續性訪問許可權,請使用 ACTION_OPEN_DOCUMENT, 例如,允許使用者編輯儲存在文件提供程式中的影象的照片編輯應用。
獲取文件
private static final int READ_REQUEST_CODE = 42; ... public void performFileSearch() { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); //過濾器只顯示可以開啟的結果 intent.addCategory(Intent.CATEGORY_OPENABLE); //使用影象MIME資料型別過濾以僅顯示影象 intent.setType("image/*"); //要搜尋通過已安裝的儲存提供商提供的所有文件 //intent.setType("*/*"); startActivityForResult(intent, READ_REQUEST_CODE); } 複製程式碼
處理結果
@Override public void onActivityResult(int requestCode, int resultCode,Intent resultData) { //使用resultdata.getdata ( )提取該URI if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) { Uri uri = null; if (resultData != null) { uri = resultData.getData(); Log.i(TAG, "Uri: " + uri.toString()); showImage(uri); } } } 複製程式碼
獲取 InputStream,獲取到InputStream我們就可以讀取該檔案了,當然還可通過URi獲取檔案的具體檔案地址
private String readTextFromUri(Uri uri) throws IOException { InputStream inputStream = getContentResolver().openInputStream(uri); BufferedReader reader = new BufferedReader(new InputStreamReader( inputStream)); StringBuilder stringBuilder = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { stringBuilder.append(line); } fileInputStream.close(); parcelFileDescriptor.close(); return stringBuilder.toString(); } 複製程式碼
保留許可權
當應用開啟檔案進行讀取或寫入時,系統會為我們的應用提供針對該檔案的 URI 授權。 該授權將一直持續到使用者裝置重啟時。但假定我們的應用是影象編輯應用,而且我們希望使用者能夠直接從應用中訪問他們編輯的最後 5 張影象。 如果使用者的裝置已經重啟,您就需要將使用者轉回系統選取器以查詢這些檔案,這顯然不是理想的做法。
為防止出現這種情況,可以保留系統為您的應用授予的許可權。 應用實際上是獲取了系統提供的持久 URI 授權。 這使使用者能夠通過您的應用持續訪問檔案,即使裝置已重啟也不受影響:
final int takeFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); // 檢查最新的資料許可權 getContentResolver().takePersistableUriPermission(uri, takeFlags); 複製程式碼
獲取檔案的真實路徑
SAF應用只能獲取到使用者所選擇檔案的Uri,有時候我們需要的卻是檔案的絕對路徑,通過下面的方式可以獲取檔案的絕對路徑,參考Stackoverflow
public static String getPath(Context context, Uri uri) { String path = null; //file: 開頭的 if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { path = uri.getPath(); return path; } // 以 content:// 開頭的,比如 content://media/extenral/images/media/17766 if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()) && Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { Cursor cursor = context.getContentResolver().query(uri, new String[]{MediaStore.Images.Media.DATA}, null, null, null); if (cursor != null) { if (cursor.moveToFirst()) { int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); if (columnIndex > -1) { path = cursor.getString(columnIndex); } } cursor.close(); } return path; } // 4.4 if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (DocumentsContract.isDocumentUri(context, uri)) { if (isExternalStorageDocument(uri)) { // ExternalStorageProvider final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; if ("primary".equalsIgnoreCase(type)) { path = Environment.getExternalStorageDirectory() + "/" + split[1]; return path; } } else if (isDownloadsDocument(uri)) { // DownloadsProvider final String id = DocumentsContract.getDocumentId(uri); final Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); path = getDataColumn(context, contentUri, null, null); return path; } else if (isMediaDocument(uri)) { // MediaProvider final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; Uri contentUri = null; if ("image".equals(type)) { contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; } else if ("video".equals(type)) { contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; } else if ("audio".equals(type)) { contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; } final String selection = "_id=?"; final String[] selectionArgs = new String[]{split[1]}; path = getDataColumn(context, contentUri, selection, selectionArgs); return path; } } } return null; } private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { Cursor cursor = null; final String column = "_data"; final String[] projection = {column}; try { cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); if (cursor != null && cursor.moveToFirst()) { final int column_index = cursor.getColumnIndexOrThrow(column); return cursor.getString(column_index); } } finally { if (cursor != null) cursor.close(); } return null; } private static boolean isExternalStorageDocument(Uri uri) { return "com.android.externalstorage.documents".equals(uri.getAuthority()); } private static boolean isDownloadsDocument(Uri uri) { return "com.android.providers.downloads.documents".equals(uri.getAuthority()); } private static boolean isMediaDocument(Uri uri) { return "com.android.providers.media.documents".equals(uri.getAuthority()); } 複製程式碼