须要訪问一个供应商的文档能够使用SAF集成,仅仅需几行代码client应用程序。
该SAF包含下面内容: 文件提供者的内容提供商同意存储服务(如谷歌驱动器)来揭示它管理的文件。文档提供者实现为DocumentsProvider类的子类。该文件提供商的架构是基于传统文件的层次结构。但怎样将文档提供物理存储的数据是由你。
Android平台包含几个内置文档提供商。例如以下载,图片和视频。
client应用程序-A调用的ACTION_OPEN_DOCUMENT和/或ACTION_CREATE_DOCUMENT意图和接收文件供应商返回的文件的自己定义应用程序。 选择器-A系统的用户界面。让全部的文件提供满足client应用程序的搜索条件的用户訪问文件。 是一些由SAF提供的功能例如以下: 同意用户从全部文件提供者,而不不过一个单一的应用程序浏览内容。 它使你的应用,以获得长期的。持续的訪问由文件供应商所拥有的文件。通过这个接入用户能够加入,编辑,保存和删除文件的供应商。 支持多个用户帐户和瞬态根。如USB存储提供商,这仅仅有在驱动器插入出现。 概观该中心SAF周围是DocumentsProvider类的子类内容提供商。内的文件提供者,数据被构造为传统的文件层次结构:
图1.文档提供数据模型。根指向一个单一的文件。然后启动扇出整个树。
请注意下面几点: 每一个文档提供报告的一个或多个“根”这是起点为探索文档的树。每一根都有一个唯一COLUMN_ROOT_ID。它指向一个文件(一个文件夹),表示根文件夹下的内容。
根是设计动态,支持使用情况下。像多个帐户,短暂的USB存储设备或用户登录/注销。
在每一根都是一个单独的文档。该文件指向1至N的文件,当中每一个依次能够指向1至N的文档。 每一个存储后端通过一个独特的COLUMN_DOCUMENT_ID引用它们的表面单个文件和文件夹。文档ID必须是唯一的。由于它们是用来在设备又一次启动持久URI补助没有改变,一旦发出。
文件能够是可打开的文件(具有特定MIME类型),或含有额外的文件的文件夹(用MIME_TYPE_DIR MIME类型)。 每一个文档能够具有不同的能力。如通过COLUMN_FLAGS说明。比如。FLAG_SUPPORTS_WRITE,FLAG_SUPPORTS_DELETE和FLAG_SUPPORTS_THUMBNAIL。同一COLUMN_DOCUMENT_ID能够包括在多个文件夹。
控制流 如上所述。文档提供者数据模型是基于传统的文件的层次结构。可是,您能够物理存储你的数据。仅仅要你喜欢,仅仅要它能够通过DocumentsProvider API进行訪问。比如。你能够使用你的数据基于标签的云存储。
图2示出的相片应用可能怎样使用SAF訪问存储的数据。比如:图2.存储訪问架构流程
请注意下面几点: 在SAF,供应商和客户不直接交互。client请求的权限与文件(即。阅读。编辑,创建或删除文件)进行交互。 当一个应用程序(在此例中,一个照片应用)触发意图ACTION_OPEN_DOCUMENT或行动CREATE_DOCUMENT交互启动。这样做的目的可能包含过滤器,以进一步细化标准,比如,“给我说有'形象'MIME类型的全部打开的文件。
”
一旦意图火灾,该系统选择器前进到每一个已注冊的提供者和显示用户的匹配内容根源。 在选择器为用户提供了訪问文档。即使底层文件提供者可能是很不同的标准接口。比如,图2显示了谷歌驱动器提供商。USB提供商和云服务提供商。 图3显示了用户在当中搜索图像选择了谷歌驱动器帐户选择器:图3.选择器
当用户选择谷歌Drive都显示的图像。如图4从这一点上,用户能够与之互动以不论什么方式被提供者和客户机应用程序的支持。图4.图片
编写client应用程序 在Android 4.3和更低的,假设你希望你的应用程序能够从还有一台应用程序文件时,它必须调用的意图。如ACTION_PICK或ACTION_GET_CONTENT。然后。用户必须选择当中一个应用程序来选择一个文件,并选择应用程序必须为用户提供的用户界面来浏览,并从可用的文件挑。 在Android 4.4及更高版本号,能够选择使用ACTION_OPEN_DOCUMENT意图。当中显示由该同意用户浏览其它应用程序已提供的全部文件系统控制的选择器UI的附加选项。从该单个用户界面中,用户能够从不论什么所支持的应用程序的选择一个文件。
ACTION_OPEN_DOCUMENT并不旨在成为ACTION_GET_CONTENT更换。你应该使用一个取决于你的应用程序的需求: 假设你希望你的应用程序仅仅需读取/导入数据使用ACTION_GET_CONTENT。用这样的方法。应用导入的数据。拷贝诸如图像文件。 假设你希望你的应用,以获得长期的。持续的訪问由文件提供者拥有的文档使用ACTION_OPEN_DOCUMENT。一个样例是一个照片编辑应用程序,同意用户编辑存储在文档图像提供商。
本节将介绍怎样依据ACTION_OPEN_DOCUMENT和ACTION_CREATE_DOCUMENT意图编写client应用程序。 搜索文件 以下的代码片断使用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);}请注意下面几点: 当应用程序触发ACTION_OPEN_DOCUMENT意图。它将启动一个显示全部匹配的文件提供者选择器。 加入类别CATEGORY_OPENABLE到意图对结果进行过滤,以便仅显示能够打开文件,如图像文件。 声明intent.setType(“图像/*”)进一步过滤仅显示有图像MIME数据类型的文档。 处理结果 一旦用户选择器中选择一种文件的onActivityResult()被调用。
指向被选择的文档的URI包括在结果数据參数。提取URI使用的getData()。
一旦你拥有它。你能够用它来获取用户想要的文件。
比如:
@Overridepublic 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); } }}检查文档元数据 一旦你的URI为一个文件,你能够訪问它的元数据。这段代码抓住由URI指定的文件元数据。并记录它:
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(); }}打开一个文档 一旦你的URI为一个文件,你能够打开它,或者你想用它做其它。 位图 以下是怎样你可能会打开一个位图的样例:
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;}请注意。你不应该做的UI线程此操作。这样做的背景下。使用AsyncTask的。一旦你打开了位图,您能够在ImageView的显示。 获取一个InputStream 这里是你怎样能得到从URI的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();}创建一个新文档 您的应用程序能够创建在使用操作CREATE_DOCUMENT意图文档提供一个新的文档。
要创建你给你的意图MIME类型和文件名称的文件,并具有独特的请求的代码启动。其余的是照应你:
// 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),这样就能够继续写吧。 删除文件 假设你有URI为一个文件和文档的Document.COLUMN flags包括SUPPORTS删除,您能够删除该文件。 比如:
DocumentsContract.deleteDocument(getContentResolver(), uri);编辑文档 您能够使用SAF到位,编辑一个文本文件。这段代码触发ACTION_OPEN_DOCUMENT意图和使用类别CATEGORY_OPENABLE以仅仅显示可打开的文档。它还过滤器仅仅显示文本文件:
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()(请參阅处理结果),你能够调用代码来运行编辑。以下的代码片段获取从ContentResolver的一个FileOutputStream。在默认情况下它使用“写入”模式。这是最好的做法。要求您须要訪问最少的。所以不要问读/写,假设你须要的是写:
private void alterDocument(Uri uri) { try { 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(); }}坚持权限 当你的应用程序打开进行读取或写入文件时。系统会给你的应用该文件的URI权限授予。它会一直持续到用户的设备又一次启动。可是。如果你的应用程序是一个图像编辑应用程序,并希望用户可以从你的应用程序訪问他们所编辑的最后5张图片。直接。如果用户的设备又一次启动后,你必须给用户发送回系统选择器来查找文件,这显然是不理想的。 为了防止这样的情况发生,你能够坚持的系统给您的应用程序的权限。实际上。你的应用程序“须要”,该系统提供了持久化的URI权限授予。这使得通过您的应用程序文件的用户继续訪问,即使该设备已经又一次启动:
final int takeFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);// Check for the freshest data.getContentResolver().takePersistableUriPermission(uri, takeFlags);还有最后一步。您可能已经保存您的应用程序訪问最新的URI,但它们可能不再有效。还有一个应用程序可能已删除或改动的文件。因此,你应该总是调用getContentResolver()。
takePersistableUriPermission()来检查最新的数据。
编写自己定义文档提供 假设您正在开发,对于文件(如云端储存服务)提供存储服务的应用程序。你能够通过编写自己定义文档提供使可通过SAF文件。本节介绍了怎样做到这一点。
表现 要实现自己定义文档提供商,下面内容加入到您的应用程序的清单: API级别19或更高的目标。 A <provider>元素声明自己定义的存储供应商。 你的供应商,这是它的类名。包含包名的名称。比如:com.example.android.storageprovider.MyCloudProvider。
你的权威,这是您的包名称的名称(在该样例中,com.example.android.storageprovider)加的内容提供者(文件)的类型。比如,com.example.android.storageprovider.documents。 属性android:导出设置为“真”。您必须导出你的供应商,使其它应用程序能够看到它。 属性android:grantUriPermissions设置为“真”。此设置同意系统授予的其它应用程序訪问内容提供商。对于怎样坚持赠款为特定文档的讨论,參见坚持权限。 该MANAGE_DOCUMENTS许可。默认情况下,供应商是提供给大家。加入该权限限制你的供应商系统。此限制是出于安全非常重要。
Android的:启用属性设置为在资源文件里定义一个布尔值。这个属性的目的是禁止在执行Android 4.3或更低的设备供应商。比如。机器人:启用=“@布尔/ atLeastKitKat”。除了包含在清单此属性,你须要做到下面几点:
在依据RES /值的bool.xml资源文件/加入此行:In yourfalse
bool.xml
resources file under res/values-v19/
, add this line: 一个意图过滤器。当中包含android.content.action.DOCUMENTS提供商的行动,让你的供应商在系统搜索提供商出如今选择器。 以下是从包含一个提供程序的演示样例清单摘录:true
执行Android4.3和更低的配套器件 该ACTION_OPEN_DOCUMENT意图是仅适用于执行Android 4.4及更高版本号的设备可用。... ....
假设你希望你的应用程序支持ACTION_GET_CONTENT,以适应正在执行的是Android 4.3和更低的设备。你应该在你的清单中禁止ACTION_GET_CONTENT意图过滤器执行Android4.4或更高版本号的设备。文档提供者和ACTION_GET_CONTENT应考虑互斥。
假设同一时候支持他们两个,你的应用程序将在系统选择器UI中出现两次。提供訪问您的存储数据的两种不同方式。这会让用户感到困惑。
这里是禁止用于执行Android版本号4.4或更高版本号的设备的ACTION_GET_CONTENT意图过滤的推荐方式: 在依据RES /值的bool.xml资源文件/加入此行In yourtrue
bool.xml
resources file under res/values-v19/
, add this line:
bool name="atMostJellyBeanMR2">false
加入活动别名禁用4.4版本号的ACTION_GET_CONTENT意图过滤器(API等级19)高。
比如:
合同 通常,当你写一个自己定义的内容提供商,任务之中的一个是实施合同类,作为内容提供商的开发者指南中所述。合同类是包括的URI,涉及到供应商常量定义。列名。MIME类型和其它元数据有public final类。新加坡武装部队提供了这些合同类你,所以你不须要编写自己的: DocumentsContract.Document DocumentsContract.Root 比如,这里有您可能会在当你的文档提供查询的文档或根光标返回列:
private static final String[] DEFAULT_ROOT_PROJECTION = new String[]{ Root.COLUMN_ROOT_ID, Root.COLUMN_MIME_TYPES, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE, Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES,};private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{ Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,};子类DocumentsProvider 编写定制文档提供下一步是继承抽象类的文档提供。
至少。你须要实现下面方法:
查询根() queryChildDocuments() queryDocument() 使用openDocument() 这些都是你实现严格要求的唯一方法,可是还有很多其它你可能想。见DocumentsProvider了解详情。 实施queryRoots 你的实现queryRoots的()必须返回指向您的文档提供的全部根文件夹中的光标。使用DocumentsContract.Root定义的列。 在以下的片段中,投影參数表示调用者想要找回特定字段。该片断创建一个新的光标,并添加了一个行吧。一个根,一个顶级文件夹。例如以下载或图像。大多数供应商仅仅能有一个根。
你可能有一个以上的。比如,在多个用户帐户的情况下。
在这样的情况下,仅仅要加入一个第二排的光标。
@Overridepublic 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;}实施queryChildDocuments 你的实现查询子文档()必须返回一个指向一个光标到指定文件夹下的全部文件,使用DocumentsContract.Document定义的列。 当你选择的选择器UI应用程序根文件夹时调用此方法。它得到的根文件夹下的一个文件夹的子文档。它能够在文件层次结构的不论什么级别被调用,而不不过根。这段代码使得与请求的列的新光标,然后将有关的父文件夹,将光标每个直系子女的信息。
一个孩子能够是图片,还有一个文件夹的不论什么文件:
@Overridepublic 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;}
实施queryDocument 你的实现查询文档()的必须返回一个指向指定的文件,使用DocumentsContract.Document定义的列光标。 该queryDocument()方法返回在查询子文档()传递同样的信息,但对于一个特定的文件:
@Overridepublic 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;}实现使用openDocument 您必须实现使用openDocument()返回表示指定文件ParcelFileDescriptor。其它应用程序能够使用返回ParcelFileDescriptor流数据。
当用户选择一个文件和client应用程序请求訪问它通过调用打开的文件描写叙述符()系统调用此方法。
比如:
@Overridepublic ParcelFileDescriptor openDocument(final String documentId, final String mode, CancellationSignal signal) throws FileNotFoundException { Log.v(TAG, "openDocument, mode: " + mode); // It's OK to do network operations in this method to download the document, // as long as you periodically check the CancellationSignal. If you have an // extremely large file to transfer from the network, a better solution may // be pipes or sockets (see ParcelFileDescriptor for helper methods). final File file = getFileForDocId(documentId); final boolean isWrite = (mode.indexOf('w') != -1); if(isWrite) { // Attach a close listener if the document is opened in write mode. try { Handler handler = new Handler(getContext().getMainLooper()); return ParcelFileDescriptor.open(file, accessMode, handler, new ParcelFileDescriptor.OnCloseListener() { @Override public void onClose(IOException e) { // Update the file with the cloud server. The client is done // writing. Log.i(TAG, "A file with id " + documentId + " has been closed! Time to " + "update the server."); } }); } catch (IOException e) { throw new FileNotFoundException("Failed to open document with id " + documentId + " and mode " + mode); } } else { return ParcelFileDescriptor.open(file, accessMode); }}安全 如果你的文档提供一个password保护的云存储服务。并要确保用户在你開始分享他们的文件之前登录。
什么应该您的应用程序做,如果用户没有登录?解决的办法是在运行查询根()返回零根。也就是说,一个空的根光标:
public Cursor queryRoots(String[] projection) throws FileNotFoundException { ... // If user is not logged in, return an empty root cursor. This removes our // provider from the list entirely. if (!isUserLoggedIn()) { return result;}还有一步骤是调用getContentResolver()。有NotifyChange()。还记得DocumentsContract? We'are用它来使这个URI。以下的代码片断告诉系统查询您的文档提供每当用户的登录状态变化的根源。假设用户没有登录。打电话查询根()返回一个空光标,如上图所看到的。
这保证了假设用户登录到提供者的提供者的文件才可用。
private void onLoginButtonClick() { loginOrLogout(); getContentResolver().notifyChange(DocumentsContract .buildRootsUri(AUTHORITY), null);}