android存儲訪問框架Storage Access Framework
在了解storage access framework 之前,我們先來看看android4.4中的一個特性。如果我們希望能選擇android手機中的一張圖片,通常都是發送一個Intent給相應的程序,一般這個程序是系統自帶的圖庫應用(如果你的手機中有兩個圖庫類的app 很可能會叫你選擇一個),這個Intent一般是這樣寫的:
Intent intent=new Intent(Intent.ACTION_GET_CONTENT);//ACTION_OPEN_DOCUMENT
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/jpeg");
使用這樣的一種方法來選擇圖片在android4.4中會直接彈出一個很漂亮的界面,有點像一個文件管理器,其實他比文件管理器更強大,他是一個內容提供器,可以按照目錄一層一層的選擇文件,也可以按照文件種類選擇文件,比如圖片、視頻、音頻等,還可以打開一個應用程序選擇文件,界面如下:
--
--
其實這是一個叫做documentsui的內置程序,因為它的manifest 沒有帶LAUNCHER的activity所以不會顯示在桌面上。
下面是正文:
Storage Access Framework
Android4.4中引入了Storage Access Framework存儲訪問框架,簡稱(SAF)。SAF為用戶瀏覽手機中存儲的內容提供了方便,這些內容不僅包括文檔、圖片,視頻、音頻、下載,而且還包括所有由特定ContentProvider(須具有約定的API)提供的內容。不管這些內容來自于哪里,不管是哪個應用調用瀏覽系統文件內容的命令,系統都會用一個統一的界面讓你去瀏覽。
這種能力姑且叫做一種生態系統,云存儲以及本地存儲都可以通過實現DocumentsProvider來參與到這個系統中。而客戶端app要使用SAF提供的服務只需幾行代碼即可。
SAF框架包括以下內容:
(1)Document provider文件內容提供方
這是一個特殊的content provider(內容提供方),他讓一個存儲服務(比如Google Drive)可以對外展示自己所管理的文件。一個Document provider其實就是實現了DocumentsProvider的子類。document-provider的schema 和傳統的文件存徑格式一致,但是至于你的內容是怎么存儲的完全取決于你自己,android系統中已經內置了幾個這樣的Document provider,比如關于下載、圖片以及視頻的Document provider。(注意這里的紅色DocumentsProvider是一個類,而分開寫的Document provider只是一種描述,因為翻譯出來可能會讓人忘了他的特殊身份。)
(2)客戶端app
一個觸發ACTION_OPEN_DOCUMENT或者ACTION_CREATE_DOCUMENTintent的客戶端軟件。通過觸發ACTION_OPEN_DOCUMENT或者ACTION_CREATE_DOCUMENT客戶端可以接收來自于Document provider的內容。
(3)選擇器Picker
選擇器其實就是一個類似于文件管理器的界面,而且是系統級別的界面,他提供了訪問滿足客戶端過濾條件的所有Document provider內容的通道。說的具體點選擇器就是文章開頭提到的documentsui程序。
SAF的一些特性:
用戶可以瀏覽所有document provider提供的內容,不光是一個app。
提供了長期、持續的訪問document provider中文件的能力以及數據的持久化,用戶可以實現添加、刪除、編輯、保存document provider所維護的內容。
支持多用戶以及臨時性的內容服務,比如USB storage providers只有當驅動安裝成功才會出現。
概要
SAF的核心是實現了DocumentsProvider的子類,即內容提供者(document provider)。document provider中數據是以傳統的文件目錄樹組織起來的:
流程圖
雖說document provider中數據是以傳統的文件目錄樹組織起來的,但是那只是對外表現的形式,至于你的數據在內部究竟是怎么樣(甚至完全雜亂無章),完全取決于你自己,只要你對外的接口能夠通過DocumentsProvider的api訪問就可以。
下面的流程圖展示了一個photo應用使用SAF可能的結構:
從上圖可以看出選擇器Picker(System UI)是一個鏈接調用者與內容提供者的橋梁。它提供了一個UI同時也告訴了調用者可以選擇哪些內容提供者,比如這里的DriveDocProvider、 UsbDocProvider 、CloundDocProvider。
當客戶端app與Document provider之間的交互是在觸發了ACTION_OPEN_DOCUMENT或者ACTION_CREATE_DOCUMENT intent之后,intent還可以進一步設置過濾條件:比如限制MIME type為’image’。
當intent觸發之后選擇器去尋找每一個注冊了的provider,并將provider的符合條件的根目錄顯示出來。
選擇器(即documentsui)為訪問不同形式、不同來源的文件提供了統一的界面,你可以看到我的文件形式可以是圖片、視頻,文件的內容可以是來自本地或者是Google Drive的云服務。
下圖顯示了用戶在選擇圖片的時候點中了Google Drive的情況。
客戶端是如何調用的
在android4.3時代,如果你想從另外一個app中選擇一個文件,比如從圖庫中選擇一張圖片文件,你必須觸發一個intent比如ACTION_PICK或者ACTION_GET_CONTENT。然后在候選的app中選擇一個app,從中獲得你想要的文件,最關鍵的是被選擇的app中要具有能為你提供文件的功能,如果一個不負責任的第三方開發者注冊了一個恰恰符合你需求的intent,但是沒有實現返回文件的功能,那么就會出現意想不到的錯誤。
在4.4中,你多了一個選擇方式,你可以發送ACTION_OPEN_DOCUMENTintent來調用系統的documentsui來選擇任何文件,不需要再依賴于其他的app了。
但是并不是說ACTION_GET_CONTENT就完全沒有用了,如果你只是打開讀取一個文件,ACTION_GET_CONTENT還是可以的,如果你是要有寫入編輯的需求,那就用ACTION_OPEN_DOCUMENT。
注: 實際上在4.4系統中ACTION_GET_CONTENT啟動的還是documentsui。
下面演示如何用ACTION_OPEN_DOCUMENT選擇一張圖片:
private static final int READ_REQUEST_CODE = 42; ... /**
- Fires an intent to spin up the "file chooser" UI and select an image.
/
public void performFileSearch() {
// ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
// browser.
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
// Filter to only show results that can be "opened", such as a
// file (as opposed to a list of contacts or timezones)
intent.addCategory(Intent.CATEGORY_OPENABLE);
// Filter to show only images, using the image MIME data type.
// If one wanted to search for ogg vorbis files, the type would be "audio/ogg".
// To search for all documents available via installed storage providers,
// it would be "/".
intent.setType("image/");
startActivityForResult(intent, READ_REQUEST_CODE);
}</pre>
ACTION_OPEN_DOCUMENT intent發出以后documentsui會顯示所有滿足條件的document provider(顯示的是他們的標題),以圖片為例,其實它對應的document provider是MediaDocumentsProvider(在系統源碼中),而訪問MediaDocumentsProvider的URi形式為com.android.providers.media.documents;
如果在intent filter中加入category CATEGORY_OPENABLE的條件,則顯示結果只有可以打開的文件,比如圖片文件(思考一下 ,哪些是不可以打開的呢?);
如果設置intent.setType("image/*")則只顯示MIME type為image的文件。
獲取返回的結果
返回結果一般是一個uri,數據保存在onActivityResult的第三個參數resultData中,通過resultData.getData()獲取uri。
@Override public void onActivityResult(int requestCode, int resultCode,
// The ACTION_OPEN_DOCUMENT intent was sent with the request code // READ_REQUEST_CODE. If the request code seen here doesn't match, it's the // response to some other intent, and the code below shouldn't run at all. if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {Intent resultData) {
} }</pre>// The document selected by the user won't be returned in the intent. // Instead, a URI to that document will be contained in the return intent // provided to this method as a parameter. // Pull that URI using resultData.getData(). Uri uri = null; if (resultData != null) { uri = resultData.getData(); Log.i(TAG, "Uri: " + uri.toString()); showImage(uri); }
獲取元數據
一旦得到uri,你就可以用uri獲取文件的元數據 。下面演示了如何得到元數據信息,并打印到log中。
public void dumpImageMetaData(Uri uri) { // The query, since it only applies to a single document, will only return // one row. There's no need to filter, sort, or select fields, since we want // all fields for one document. Cursor cursor = getActivity().getContentResolver()
try { // moveToFirst() returns false if the cursor has 0 rows. Very handy for // "if there's anything to look at, look at it" conditionals..query(uri, null, null, null, null, null);
} finally {if (cursor != null && cursor.moveToFirst()) { // Note it's called "Display Name". This is // provider-specific, and might not necessarily be the file name. String displayName = cursor.getString( cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); Log.i(TAG, "Display Name: " + displayName); int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE); // If the size is unknown, the value stored is null. But since an // int can't be null in Java, the behavior is implementation-specific, // which is just a fancy term for "unpredictable". So as // a rule, check if it's null before assigning to an int. This will // happen often: The storage API allows for remote files, whose // size might not be locally known. String size = null; if (!cursor.isNull(sizeIndex)) { // Technically the column stores an int, but cursor.getString() // will do the conversion automatically. size = cursor.getString(sizeIndex); } else { size = "Unknown"; } Log.i(TAG, "Size: " + size); }
} }</pre>cursor.close();
還可以獲得bitmap(這段代碼我也沒看懂):
private Bitmap getBitmapFromUri(Uri uri) throws IOException { ParcelFileDescriptor parcelFileDescriptor =
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor); parcelFileDescriptor.close(); return image;</pre>getContentResolver().openFileDescriptor(uri, "r");
獲得輸出流
private String readTextFromUri(Uri uri) throws IOException { InputStream inputStream = getContentResolver().openInputStream(uri); BufferedReader reader = new BufferedReader(new InputStreamReader(
StringBuilder stringBuilder = new StringBuilder(); String line; while ((line = reader.readLine()) != null) {inputStream));
} fileInputStream.close(); parcelFileDescriptor.close(); return stringBuilder.toString(); }</pre>stringBuilder.append(line);
上面的獲取元數據、bitmap、輸出流的代碼和SAF并沒有什么關系,只是告訴你通過一個Uri你可以知道什么,而Uri的獲取是利用SAF得到的。
如何創建一個新的文件
使用ACTION_CREATE_DOCUMENT intent來創建文件
// Here are some examples of how you might call this method. // The first parameter is the MIME type, and the second parameter is the name // of the file you are creating: // // createFile("text/plain", "foobar.txt"); // createFile("image/png", "mypicture.png"); // Unique request code. private static final int WRITE_REQUEST_CODE = 43; ... private void createFile(String mimeType, String fileName) { Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); // Filter to only show results that can be "opened", such as // a file (as opposed to a list of contacts or timezones). intent.addCategory(Intent.CATEGORY_OPENABLE); // Create a file with the requested MIME type. intent.setType(mimeType); intent.putExtra(Intent.EXTRA_TITLE, fileName); startActivityForResult(intent, WRITE_REQUEST_CODE); }
可以在onActivityResult()中獲取被創建文件的uri。
刪除文件
前提是Document.COLUMN_FLAGS包含SUPPORTS_DELETE
DocumentsContract.deleteDocument(getContentResolver(), uri);
實現自己的document provider
如果你希望自己應用的數據也能在documentsui中打開,你就需要寫一個自己的document provider。下面介紹自定義DocumentsProvider的步驟。
api 為19+
首先你需要在manifest文件中聲明有這樣一個Provider:
Provider的name為類名加包名,比如:
com.example.android.storageprovider.MyCloudProvider
Authority為包名+provider的類型名,如:
Com.example.android.storageprovider.documents
android:exported屬性的值為ture
下面是一個provider的例子寫法:
<manifest... > ... <uses-sdk
</application> </manifest></pre>android:minSdkVersion="19" android:targetSdkVersion="19" /> .... <provider android:name="com.example.android.storageprovider.MyCloudProvider" android:authorities="com.example.android.storageprovider.documents" android:grantUriPermissions="true" android:exported="true" android:permission="android.permission.MANAGE_DOCUMENTS" android:enabled="@bool/atLeastKitKat"> <intent-filter> <action android:name="android.content.action.DOCUMENTS_PROVIDER" /> </intent-filter> </provider>
DocumentsProvider的子類
你至少要實現如下幾個方法:
queryRoots()
queryChildDocuments()
queryDocument()
openDocument()
還有些其他的方法,但并不是必須的。下面演示一個實現訪問文件(file)系統的DocumentsProvider的大致寫法。
queryRoots的實現:
@Override public Cursor queryRoots(String[] projection) throws FileNotFoundException { // Create a cursor with either the requested fields, or the default // projection if "projection" is null. final MatrixCursor result =
// If user is not logged in, return an empty root cursor. This removes our // provider from the list entirely. if (!isUserLoggedIn()) {new MatrixCursor(resolveRootProjection(projection));
} // It's possible to have multiple roots (e.g. for multiple accounts in the // same app) -- just add multiple cursor rows. // Construct one row for a root called "MyCloud". final MatrixCursor.RowBuilder row = result.newRow(); row.add(Root.COLUMN_ROOT_ID, ROOT); row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.root_summary)); // FLAG_SUPPORTS_CREATE means at least one directory under the root supports // creating documents. FLAG_SUPPORTS_RECENTS means your application's most // recently used documents will show up in the "Recents" category. // FLAG_SUPPORTS_SEARCH allows users to search all documents the application // shares. row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |return result;
// COLUMN_TITLE is the root title (e.g. Gallery, Drive). row.add(Root.COLUMN_TITLE, getContext().getString(R.string.title)); // This document id cannot change once it's shared. row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(mBaseDir)); // The child MIME types are used to filter the roots and only present to the // user roots that contain the desired type somewhere in their file hierarchy. row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(mBaseDir)); row.add(Root.COLUMN_AVAILABLE_BYTES, mBaseDir.getFreeSpace()); row.add(Root.COLUMN_ICON, R.drawable.ic_launcher); return result; }</pre>Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_SEARCH);
queryChildDocuments的實現
@Override public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
final MatrixCursor result = newString sortOrder) throws FileNotFoundException {
final File parent = getFileForDocId(parentDocumentId); for (File file : parent.listFiles()) {MatrixCursor(resolveDocumentProjection(projection));
} return result; }</pre>// Adds the file's display name, MIME type, size, and so on. includeFile(result, null, file);
queryDocument的實現
@Override public Cursor queryDocument(String documentId, String[] projection) throws
// Create a cursor with the requested projection, or the default projection. final MatrixCursor result = newFileNotFoundException {
includeFile(result, documentId, null); return result; }</pre>MatrixCursor(resolveDocumentProjection(projection));
為了更好的理解這篇文章,可以參考下面這些鏈接。
參考文章
https://developer.android.com/guide/topics/providers/document-provider.htm這篇文章的英文原文國內或許不能訪問
http://blog.csdn.net/huangyanan1989/article/details/17263203Android4.4中獲取資源路徑問題 因為Storage Access Framework而引起的
https://github.com/iPaulPro/aFileChooser 一個文件管理器,在4.4中他是直接啟用了documentsui
https://github.com/ianhanniballake/LocalStorage一個自定義的DocumentsProvider
https://github.com/xin3liang/platform_packages_providers_MediaProvider 實現了查詢多媒體文件的DocumentsProvider,包括查詢圖片,這個是系統里面的