Storage Access Framework

Android API Guide

Android 4.4 引入了存储访问框架Storage Access Framework(SAF),云或本地存储服务可以实现DocumentProvider来分装他们的服务以便加入到该生态系统中。

SAF包含了
Document Provider, Android 包含几个内置的DocumentsProvider,注入下载Downloads, Images, Videos.
Client App: 一个自定义的App,可发送ACTION_OPEN_DOCUMENT或者ACTION_CREATE_DOCUMENT,接收返回的文件。
Picker: 一个系统UI, 允许用户访问所有document providers 满足client app的搜索条件的。

Overview

架构图
这里写图片描述

  • 以Root开始,指向单一节点,然后改单一节点再扇形展开。每个Root都有一个独一无二的COLUMN_ROOT_ID, 并且指向一个document或者directory, 代表了该Root下的内容。
  • 在每个root下是一个single document, 该document指向1 to N个documents, 这下面的每个再指向1 to N个documents。
  • 每个文件或者目录 都有COLUMN_DOCUMENT_ID, 该ID必须是独一无二的,且在issue后不可修改的。
  • documents 可以是可打开的文件,也可是目录。
  • 每个document 可以具有不同的能力,由COLUMN_FLAGS指定。例如:FLAG_SUPPORTS_WRITE, FLAG_SUPPORTS_DELETE, and FLAG_SUPPORTS_THUMBNAIL. 此外,COLUMN_DOCUMENT_ID可以出现在多个目录下。

Control Flow

document provider 的数据模型是基于传统的文件架构。但是,物理上,你可以随意存储,只要可以被DocumentProvider API访问。

下图显示了一个Photo App如何使用SAF访问图片
Storage Access Framework_第1张图片

注意到:
1. Client and Provider 不直接交互。
2. 当客户端发起一个Intent(ACTION_OPEN_DOCUMENT or ACTION_CREATE_DOCUMENT), 可能包括一些filters
3. 一旦Intent发出, the system picker 就会到每个注册的provider,并显示匹配结果。
4. Picker提供一个标准的接口访问文件。

Writing a Client App

在Android 4.3之前,使用ACTION_PICK或者ACTION_GET_CONTENT 获得文件,对于后者是仅copy一份文件。
在Android 4.4及更高版本,将可使用ACTION_OPEN_DOCUMENT来获取文件,是直接使用源文件。

Search for documents

发起搜索图片请求

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);
}

处理搜索结果

@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);
        }
    }
}

Note: 不会直接返回文件,而是返回文件的URI地址。

既然已经获得文件的URI,那么就可以查询该URI指向源文件的metadata。
Examine document metadata

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

打开文档

比如打开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;
}

do it in background using AsyncTask.

再比如获得一个InputStream

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

创建一个文档

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

删除一个文档

DocumentsContract.deleteDocument(getContentResolver(), uri);

注意,除了要有需要删除的文件uri, 该uri指向的文件的Document.COLUMN_FLAGS包含SUPPORTS_DELETE。

编辑一个文件:

private static final int EDIT_REQUEST_CODE = 44;
/** * Open a file for writing and append some text to it. */
 private void editDocument() {
    // 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 text files.
    intent.setType("text/plain");

    startActivityForResult(intent, EDIT_REQUEST_CODE);
}
// onActivityResult() 的处理中,调用以下方法,然后并将其写出。
private void alterDocument(Uri uri) {
    try {
        // 获得uri指向文件的 序列化文件描述符
        ParcelFileDescriptor pfd = getActivity().getContentResolver().
                openFileDescriptor(uri, "w");
        // 根据文件描述符创建文件输出流。
        FileOutputStream fileOutputStream =
                new FileOutputStream(pfd.getFileDescriptor());
        fileOutputStream.write(("Overwritten by MyCloud at " +
                System.currentTimeMillis() + "\n").getBytes());
        // Let the document provider know you're done by closing the stream.
        fileOutputStream.close();
        pfd.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

在原文中,接下来是写一个自定义的Document Provider
Writing a Custom Document Provider,不再赘述,以后有时间再看。

你可能感兴趣的:(Storage Access Framework)