android存儲訪問框架Storage Access Framework

auguest 8年前發布 | 26K 次閱讀 Android開發 移動開發

在了解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 沒有帶LAUNCHERactivity所以不會顯示在桌面上。

下面是正文:

Storage Access Framework

Android4.4中引入了Storage Access Framework存儲訪問框架,簡稱(SAF)。SAF為用戶瀏覽手機中存儲的內容提供了方便,這些內容不僅包括文檔、圖片,視頻、音頻、下載,而且還包括所有由特定ContentProvider(須具有約定的API)提供的內容。不管這些內容來自于哪里,不管是哪個應用調用瀏覽系統文件內容的命令,系統都會用一個統一的界面讓你去瀏覽。

 

這種能力姑且叫做一種生態系統,云存儲以及本地存儲都可以通過實DocumentsProvider來參與到這個系統中。而客戶端app要使用SAF提供的服務只需幾行代碼即可。

SAF框架包括以下內容:

1Document 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中數據是以傳統的文件目錄樹組織起來的,但是那只是對外表現的形式,至于你的數據在內部究竟是怎么樣(甚至完全雜亂無章),完全取決于你自己,只要你對外的接口能夠通過DocumentsProviderapi訪問就可以。

下面的流程圖展示了一個photo應用使用SAF可能的結構:

從上圖可以看出選擇器PickerSystem UI)是一個鏈接調用者與內容提供者的橋梁。它提供了一個UI同時也告訴了調用者可以選擇哪些內容提供者,比如這里的DriveDocProviderUsbDocProvider CloundDocProvider

當客戶端appDocument provider之間的交互是在觸發了ACTION_OPEN_DOCUMENT或者ACTION_CREATE_DOCUMENT intent之后,intent還可以進一步設置過濾條件:比如限制MIME typeimage

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 providerMediaDocumentsProvider(在系統源碼中),而訪問MediaDocumentsProviderURi形式為com.android.providers.media.documents

    如果在intent filter中加入category CATEGORY_OPENABLE的條件,則顯示結果只有可以打開的文件,比如圖片文件(思考一下 ,哪些是不可以打開的呢?);

    如果設置intent.setType("image/*")則只顯示MIME typeimage的文件。

    獲取返回的結果

    返回結果一般是一個uri,數據保存在onActivityResult的第三個參數resultData中,通過resultData.getData()獲取uri

    @Override
    public void onActivityResult(int requestCode, int resultCode,
    
     Intent resultData) {
    
    // 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) {
     // 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);
     }
    
    } }</pre>

    獲取元數據

    一旦得到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()
    
         .query(uri, null, null, null, null, null);
    
    try { // moveToFirst() returns false if the cursor has 0 rows. Very handy for // "if there's anything to look at, look at it" conditionals.
     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);
     }
    
    } finally {
     cursor.close();
    
    } }</pre>

    還可以獲得bitmap(這段代碼我也沒看懂):

    private Bitmap getBitmapFromUri(Uri uri) throws IOException {
     ParcelFileDescriptor parcelFileDescriptor =
    
         getContentResolver().openFileDescriptor(uri, "r");
    
    FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor); parcelFileDescriptor.close(); return image;</pre>

    獲得輸出流

    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(); }</pre>

    上面的獲取元數據、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
    
     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>
    
    </application> </manifest></pre>

    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 =
    
         new MatrixCursor(resolveRootProjection(projection));
    
    // If user is not logged in, return an empty root cursor. This removes our // provider from the list entirely. if (!isUserLoggedIn()) {
     return result;
    
    } // 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 |
         Root.FLAG_SUPPORTS_RECENTS |
         Root.FLAG_SUPPORTS_SEARCH);
    
    // 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>


    queryChildDocuments的實現

    @Override
    public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
    
                           String sortOrder) throws FileNotFoundException {
    
    final MatrixCursor result = new
         MatrixCursor(resolveDocumentProjection(projection));
    
    final File parent = getFileForDocId(parentDocumentId); for (File file : parent.listFiles()) {
     // Adds the file's display name, MIME type, size, and so on.
     includeFile(result, null, file);
    
    } return result; }</pre>

    queryDocument的實現

    @Override
    public Cursor queryDocument(String documentId, String[] projection) throws
    
     FileNotFoundException {
    
    // Create a cursor with the requested projection, or the default projection. final MatrixCursor result = new
         MatrixCursor(resolveDocumentProjection(projection));
    
    includeFile(result, documentId, null); return result; }</pre>

    為了更好的理解這篇文章,可以參考下面這些鏈接。

     

    參考文章

    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,包括查詢圖片,這個是系統里面的

     

 本文由用戶 auguest 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!