android documentscontract 源码,DocumentsContract.java

/*

* Copyright (C) 2013 The Android Open Source Project

*

* Licensed under the Apache License, Version 2.0 (the "License");

* you may not use this file except in compliance with the License.

* You may obtain a copy of the License at

*

* http://www.apache.org/licenses/LICENSE-2.0

*

* Unless required by applicable law or agreed to in writing, software

* distributed under the License is distributed on an "AS IS" BASIS,

* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

* See the License for the specific language governing permissions and

* limitations under the License.

*/

package android.provider;

import static com.android.internal.util.Preconditions.checkCollectionElementsNotNull;

import static com.android.internal.util.Preconditions.checkCollectionNotEmpty;

import android.annotation.NonNull;

import android.annotation.Nullable;

import android.annotation.SystemApi;

import android.annotation.UnsupportedAppUsage;

import android.content.ContentInterface;

import android.content.ContentResolver;

import android.content.Context;

import android.content.Intent;

import android.content.IntentSender;

import android.content.MimeTypeFilter;

import android.content.pm.ResolveInfo;

import android.content.res.AssetFileDescriptor;

import android.database.Cursor;

import android.graphics.Bitmap;

import android.graphics.ImageDecoder;

import android.graphics.Point;

import android.media.ExifInterface;

import android.net.Uri;

import android.os.Build;

import android.os.Bundle;

import android.os.CancellationSignal;

import android.os.OperationCanceledException;

import android.os.Parcel;

import android.os.ParcelFileDescriptor;

import android.os.ParcelFileDescriptor.OnCloseListener;

import android.os.Parcelable;

import android.os.ParcelableException;

import android.os.RemoteException;

import android.util.Log;

import com.android.internal.util.Preconditions;

import dalvik.system.VMRuntime;

import java.io.File;

import java.io.FileNotFoundException;

import java.io.IOException;

import java.util.ArrayList;

import java.util.List;

import java.util.Objects;

/**

* Defines the contract between a documents provider and the platform.

*

* To create a document provider, extend {@link DocumentsProvider}, which

* provides a foundational implementation of this contract.

*

* All client apps must hold a valid URI permission grant to access documents,

* typically issued when a user makes a selection through

* {@link Intent#ACTION_OPEN_DOCUMENT}, {@link Intent#ACTION_CREATE_DOCUMENT},

* or {@link Intent#ACTION_OPEN_DOCUMENT_TREE}.

*

* @see DocumentsProvider

*/

public final class DocumentsContract {

private static final String TAG = "DocumentsContract";

// content://com.example/root/

// content://com.example/root/sdcard/

// content://com.example/root/sdcard/recent/

// content://com.example/root/sdcard/search/?query=pony

// content://com.example/document/12/

// content://com.example/document/12/children/

// content://com.example/tree/12/document/24/

// content://com.example/tree/12/document/24/children/

private DocumentsContract() {

}

/**

* Intent action used to identify {@link DocumentsProvider} instances. This

* is used in the {@code } of a {@code }.

*/

public static final String PROVIDER_INTERFACE = "android.content.action.DOCUMENTS_PROVIDER";

/** {@hide} */

@Deprecated

public static final String EXTRA_PACKAGE_NAME = Intent.EXTRA_PACKAGE_NAME;

/**

* The value is decide whether to show advance mode or not.

* If the value is true, the local/device storage root must be

* visible in DocumentsUI.

*

* {@hide}

*/

@SystemApi

public static final String EXTRA_SHOW_ADVANCED = "android.provider.extra.SHOW_ADVANCED";

/** {@hide} */

public static final String EXTRA_TARGET_URI = "android.content.extra.TARGET_URI";

/**

* Key for {@link DocumentsProvider} to query display name is matched.

* The match of display name is partial matching and case-insensitive.

* Ex: The value is "o", the display name of the results will contain

* both "foo" and "Open".

*

* @see DocumentsProvider#querySearchDocuments(String, String[],

* Bundle)

*/

public static final String QUERY_ARG_DISPLAY_NAME = "android:query-arg-display-name";

/**

* Key for {@link DocumentsProvider} to query mime types is matched.

* The value is a string array, it can support different mime types.

* Each items will be treated as "OR" condition. Ex: {"image/*" ,

* "video/*"}. The mime types of the results will contain both image

* type and video type.

*

* @see DocumentsProvider#querySearchDocuments(String, String[],

* Bundle)

*/

public static final String QUERY_ARG_MIME_TYPES = "android:query-arg-mime-types";

/**

* Key for {@link DocumentsProvider} to query the file size in bytes is

* larger than the value.

*

* @see DocumentsProvider#querySearchDocuments(String, String[],

* Bundle)

*/

public static final String QUERY_ARG_FILE_SIZE_OVER = "android:query-arg-file-size-over";

/**

* Key for {@link DocumentsProvider} to query the last modified time

* is newer than the value. The unit is in milliseconds since

* January 1, 1970 00:00:00.0 UTC.

*

* @see DocumentsProvider#querySearchDocuments(String, String[],

* Bundle)

* @see Document#COLUMN_LAST_MODIFIED

*/

public static final String QUERY_ARG_LAST_MODIFIED_AFTER =

"android:query-arg-last-modified-after";

/**

* Key for {@link DocumentsProvider} to decide whether the files that

* have been added to MediaStore should be excluded. If the value is

* true, exclude them. Otherwise, include them.

*

* @see DocumentsProvider#querySearchDocuments(String, String[],

* Bundle)

*/

public static final String QUERY_ARG_EXCLUDE_MEDIA = "android:query-arg-exclude-media";

/**

* Sets the desired initial location visible to user when file chooser is shown.

*

*

Applicable to {@link Intent} with actions:

**

{@link Intent#ACTION_OPEN_DOCUMENT}

*

{@link Intent#ACTION_CREATE_DOCUMENT}

*

{@link Intent#ACTION_OPEN_DOCUMENT_TREE}

*

*

*

Location should specify a document URI or a tree URI with document ID. If

* this URI identifies a non-directory, document navigator will attempt to use the parent

* of the document as the initial location.

*

*

The initial location is system specific if this extra is missing or document navigator

* failed to locate the desired initial location.

*/

public static final String EXTRA_INITIAL_URI = "android.provider.extra.INITIAL_URI";

/**

* Set this in a DocumentsUI intent to cause a package's own roots to be

* excluded from the roots list.

*/

public static final String EXTRA_EXCLUDE_SELF = "android.provider.extra.EXCLUDE_SELF";

/**

* An extra number of degrees that an image should be rotated during the

* decode process to be presented correctly.

*

* @see AssetFileDescriptor#getExtras()

* @see android.provider.MediaStore.Images.ImageColumns#ORIENTATION

*/

public static final String EXTRA_ORIENTATION = "android.provider.extra.ORIENTATION";

/**

* Overrides the default prompt text in DocumentsUI when set in an intent.

*/

public static final String EXTRA_PROMPT = "android.provider.extra.PROMPT";

/**

* Action of intent issued by DocumentsUI when user wishes to open/configure/manage a particular

* document in the provider application.

*

*

When issued, the intent will include the URI of the document as the intent data.

*

*

A provider wishing to provide support for this action should do two things.

*

Add an {@code } matching this action.

*

When supplying information in {@link DocumentsProvider#queryChildDocuments}, include

* {@link Document#FLAG_SUPPORTS_SETTINGS} in the flags for each document that supports

* settings.

*/

public static final String

ACTION_DOCUMENT_SETTINGS = "android.provider.action.DOCUMENT_SETTINGS";

/**

* The action to manage document in Downloads root in DocumentsUI.

* {@hide}

*/

@SystemApi

public static final String ACTION_MANAGE_DOCUMENT = "android.provider.action.MANAGE_DOCUMENT";

/**

* The action to launch the settings of this root.

* {@hide}

*/

@SystemApi

public static final String

ACTION_DOCUMENT_ROOT_SETTINGS = "android.provider.action.DOCUMENT_ROOT_SETTINGS";

/** {@hide} */

public static final String EXTERNAL_STORAGE_PROVIDER_AUTHORITY =

"com.android.externalstorage.documents";

/** {@hide} */

public static final String EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID = "primary";

/** {@hide} */

public static final String PACKAGE_DOCUMENTS_UI = "com.android.documentsui";

/**

* Get string array identifies the type or types of metadata returned

* using DocumentsContract#getDocumentMetadata.

*

* @see #getDocumentMetadata(ContentInterface, Uri)

*/

public static final String METADATA_TYPES = "android:documentMetadataTypes";

/**

* Get Exif information using DocumentsContract#getDocumentMetadata.

*

* @see #getDocumentMetadata(ContentInterface, Uri)

*/

public static final String METADATA_EXIF = "android:documentExif";

/**

* Get total count of all documents currently stored under the given

* directory tree. Only valid for {@link Document#MIME_TYPE_DIR} documents.

*

* @see #getDocumentMetadata(ContentInterface, Uri)

*/

public static final String METADATA_TREE_COUNT = "android:metadataTreeCount";

/**

* Get total size of all documents currently stored under the given

* directory tree. Only valid for {@link Document#MIME_TYPE_DIR} documents.

*

* @see #getDocumentMetadata(ContentInterface, Uri)

*/

public static final String METADATA_TREE_SIZE = "android:metadataTreeSize";

/**

* Constants related to a document, including {@link Cursor} column names

* and flags.

*

* A document can be either an openable stream (with a specific MIME type),

* or a directory containing additional documents (with the

* {@link #MIME_TYPE_DIR} MIME type). A directory represents the top of a

* subtree containing zero or more documents, which can recursively contain

* even more documents and directories.

*

* All columns are read-only to client applications.

*/

public final static class Document {

private Document() {

}

/**

* Unique ID of a document. This ID is both provided by and interpreted

* by a {@link DocumentsProvider}, and should be treated as an opaque

* value by client applications. This column is required.

*

* Each document must have a unique ID within a provider, but that

* single document may be included as a child of multiple directories.

*

* A provider must always return durable IDs, since they will be used to

* issue long-term URI permission grants when an application interacts

* with {@link Intent#ACTION_OPEN_DOCUMENT} and

* {@link Intent#ACTION_CREATE_DOCUMENT}.

*

* Type: STRING

*/

public static final String COLUMN_DOCUMENT_ID = "document_id";

/**

* Concrete MIME type of a document. For example, "image/png" or

* "application/pdf" for openable files. A document can also be a

* directory containing additional documents, which is represented with

* the {@link #MIME_TYPE_DIR} MIME type. This column is required.

*

* Type: STRING

*

* @see #MIME_TYPE_DIR

*/

public static final String COLUMN_MIME_TYPE = "mime_type";

/**

* Display name of a document, used as the primary title displayed to a

* user. This column is required.

*

* Type: STRING

*/

public static final String COLUMN_DISPLAY_NAME = OpenableColumns.DISPLAY_NAME;

/**

* Summary of a document, which may be shown to a user. This column is

* optional, and may be {@code null}.

*

* Type: STRING

*/

public static final String COLUMN_SUMMARY = "summary";

/**

* Timestamp when a document was last modified, in milliseconds since

* January 1, 1970 00:00:00.0 UTC. This column is required, and may be

* {@code null} if unknown. A {@link DocumentsProvider} can update this

* field using events from {@link OnCloseListener} or other reliable

* {@link ParcelFileDescriptor} transports.

*

* Type: INTEGER (long)

*

* @see System#currentTimeMillis()

*/

public static final String COLUMN_LAST_MODIFIED = "last_modified";

/**

* Specific icon resource ID for a document. This column is optional,

* and may be {@code null} to use a platform-provided default icon based

* on {@link #COLUMN_MIME_TYPE}.

*

* Type: INTEGER (int)

*/

public static final String COLUMN_ICON = "icon";

/**

* Flags that apply to a document. This column is required.

*

* Type: INTEGER (int)

*

* @see #FLAG_SUPPORTS_WRITE

* @see #FLAG_SUPPORTS_DELETE

* @see #FLAG_SUPPORTS_THUMBNAIL

* @see #FLAG_DIR_PREFERS_GRID

* @see #FLAG_DIR_PREFERS_LAST_MODIFIED

* @see #FLAG_VIRTUAL_DOCUMENT

* @see #FLAG_SUPPORTS_COPY

* @see #FLAG_SUPPORTS_MOVE

* @see #FLAG_SUPPORTS_REMOVE

*/

public static final String COLUMN_FLAGS = "flags";

/**

* Size of a document, in bytes, or {@code null} if unknown. This column

* is required.

*

* Type: INTEGER (long)

*/

public static final String COLUMN_SIZE = OpenableColumns.SIZE;

/**

* MIME type of a document which is a directory that may contain

* additional documents.

*

* @see #COLUMN_MIME_TYPE

*/

public static final String MIME_TYPE_DIR = "vnd.android.document/directory";

/**

* Flag indicating that a document can be represented as a thumbnail.

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#getDocumentThumbnail(ContentInterface, Uri,

* Point, CancellationSignal)

* @see DocumentsProvider#openDocumentThumbnail(String, Point,

* android.os.CancellationSignal)

*/

public static final int FLAG_SUPPORTS_THUMBNAIL = 1;

/**

* Flag indicating that a document supports writing.

*

* When a document is opened with {@link Intent#ACTION_OPEN_DOCUMENT},

* the calling application is granted both

* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and

* {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. However, the actual

* writability of a document may change over time, for example due to

* remote access changes. This flag indicates that a document client can

* expect {@link ContentResolver#openOutputStream(Uri)} to succeed.

*

* @see #COLUMN_FLAGS

*/

public static final int FLAG_SUPPORTS_WRITE = 1 << 1;

/**

* Flag indicating that a document is deletable.

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#deleteDocument(ContentInterface, Uri)

* @see DocumentsProvider#deleteDocument(String)

*/

public static final int FLAG_SUPPORTS_DELETE = 1 << 2;

/**

* Flag indicating that a document is a directory that supports creation

* of new files within it. Only valid when {@link #COLUMN_MIME_TYPE} is

* {@link #MIME_TYPE_DIR}.

*

* @see #COLUMN_FLAGS

* @see DocumentsProvider#createDocument(String, String, String)

*/

public static final int FLAG_DIR_SUPPORTS_CREATE = 1 << 3;

/**

* Flag indicating that a directory prefers its contents be shown in a

* larger format grid. Usually suitable when a directory contains mostly

* pictures. Only valid when {@link #COLUMN_MIME_TYPE} is

* {@link #MIME_TYPE_DIR}.

*

* @see #COLUMN_FLAGS

*/

public static final int FLAG_DIR_PREFERS_GRID = 1 << 4;

/**

* Flag indicating that a directory prefers its contents be sorted by

* {@link #COLUMN_LAST_MODIFIED}. Only valid when

* {@link #COLUMN_MIME_TYPE} is {@link #MIME_TYPE_DIR}.

*

* @see #COLUMN_FLAGS

*/

public static final int FLAG_DIR_PREFERS_LAST_MODIFIED = 1 << 5;

/**

* Flag indicating that a document can be renamed.

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#renameDocument(ContentInterface, Uri, String)

* @see DocumentsProvider#renameDocument(String, String)

*/

public static final int FLAG_SUPPORTS_RENAME = 1 << 6;

/**

* Flag indicating that a document can be copied to another location

* within the same document provider.

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#copyDocument(ContentInterface, Uri, Uri)

* @see DocumentsProvider#copyDocument(String, String)

*/

public static final int FLAG_SUPPORTS_COPY = 1 << 7;

/**

* Flag indicating that a document can be moved to another location

* within the same document provider.

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#moveDocument(ContentInterface, Uri, Uri, Uri)

* @see DocumentsProvider#moveDocument(String, String, String)

*/

public static final int FLAG_SUPPORTS_MOVE = 1 << 8;

/**

* Flag indicating that a document is virtual, and doesn't have byte

* representation in the MIME type specified as {@link #COLUMN_MIME_TYPE}.

*

*

Virtual documents must have at least one alternative streamable

* format via {@link DocumentsProvider#openTypedDocument}

*

* @see #COLUMN_FLAGS

* @see #COLUMN_MIME_TYPE

* @see DocumentsProvider#openTypedDocument(String, String, Bundle,

* android.os.CancellationSignal)

* @see DocumentsProvider#getDocumentStreamTypes(String, String)

*/

public static final int FLAG_VIRTUAL_DOCUMENT = 1 << 9;

/**

* Flag indicating that a document can be removed from a parent.

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#removeDocument(ContentInterface, Uri, Uri)

* @see DocumentsProvider#removeDocument(String, String)

*/

public static final int FLAG_SUPPORTS_REMOVE = 1 << 10;

/**

* Flag indicating that a document has settings that can be configured by user.

*

* @see #COLUMN_FLAGS

* @see #ACTION_DOCUMENT_SETTINGS

*/

public static final int FLAG_SUPPORTS_SETTINGS = 1 << 11;

/**

* Flag indicating that a Web link can be obtained for the document.

*

* @see #COLUMN_FLAGS

* @see DocumentsProvider#createWebLinkIntent(String, Bundle)

*/

public static final int FLAG_WEB_LINKABLE = 1 << 12;

/**

* Flag indicating that a document is not complete, likely its

* contents are being downloaded. Partial files cannot be opened,

* copied, moved in the UI. But they can be deleted and retried

* if they represent a failed download.

*

* @see #COLUMN_FLAGS

*/

public static final int FLAG_PARTIAL = 1 << 13;

/**

* Flag indicating that a document has available metadata that can be read

* using DocumentsContract#getDocumentMetadata

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#getDocumentMetadata(ContentInterface, Uri)

*/

public static final int FLAG_SUPPORTS_METADATA = 1 << 14;

}

/**

* Constants related to a root of documents, including {@link Cursor} column

* names and flags. A root is the start of a tree of documents, such as a

* physical storage device, or an account. Each root starts at the directory

* referenced by {@link Root#COLUMN_DOCUMENT_ID}, which can recursively

* contain both documents and directories.

*

* All columns are read-only to client applications.

*/

public final static class Root {

private Root() {

}

/**

* Unique ID of a root. This ID is both provided by and interpreted by a

* {@link DocumentsProvider}, and should be treated as an opaque value

* by client applications. This column is required.

*

* Type: STRING

*/

public static final String COLUMN_ROOT_ID = "root_id";

/**

* Flags that apply to a root. This column is required.

*

* Type: INTEGER (int)

*

* @see #FLAG_LOCAL_ONLY

* @see #FLAG_SUPPORTS_CREATE

* @see #FLAG_SUPPORTS_RECENTS

* @see #FLAG_SUPPORTS_SEARCH

*/

public static final String COLUMN_FLAGS = "flags";

/**

* Icon resource ID for a root. This column is required.

*

* Type: INTEGER (int)

*/

public static final String COLUMN_ICON = "icon";

/**

* Title for a root, which will be shown to a user. This column is

* required. For a single storage service surfacing multiple accounts as

* different roots, this title should be the name of the service.

*

* Type: STRING

*/

public static final String COLUMN_TITLE = "title";

/**

* Summary for this root, which may be shown to a user. This column is

* optional, and may be {@code null}. For a single storage service

* surfacing multiple accounts as different roots, this summary should

* be the name of the account.

*

* Type: STRING

*/

public static final String COLUMN_SUMMARY = "summary";

/**

* Document which is a directory that represents the top directory of

* this root. This column is required.

*

* Type: STRING

*

* @see Document#COLUMN_DOCUMENT_ID

*/

public static final String COLUMN_DOCUMENT_ID = "document_id";

/**

* Number of bytes available in this root. This column is optional, and

* may be {@code null} if unknown or unbounded.

*

* Type: INTEGER (long)

*/

public static final String COLUMN_AVAILABLE_BYTES = "available_bytes";

/**

* Capacity of a root in bytes. This column is optional, and may be

* {@code null} if unknown or unbounded.

*

* Type: INTEGER (long)

*/

public static final String COLUMN_CAPACITY_BYTES = "capacity_bytes";

/**

* MIME types supported by this root. This column is optional, and if

* {@code null} the root is assumed to support all MIME types. Multiple

* MIME types can be separated by a newline. For example, a root

* supporting audio might return "audio/*\napplication/x-flac".

*

* Type: STRING

*/

public static final String COLUMN_MIME_TYPES = "mime_types";

/**

* Query arguments supported by this root. This column is optional

* and related to {@link #COLUMN_FLAGS} and {@link #FLAG_SUPPORTS_SEARCH}.

* If the flags include {@link #FLAG_SUPPORTS_SEARCH}, and the column is

* {@code null}, the root is assumed to support {@link #QUERY_ARG_DISPLAY_NAME}

* search of {@link Document#COLUMN_DISPLAY_NAME}. Multiple query arguments

* can be separated by a newline. For example, a root supporting

* {@link #QUERY_ARG_MIME_TYPES} and {@link #QUERY_ARG_DISPLAY_NAME} might

* return "android:query-arg-mime-types\nandroid:query-arg-display-name".

*

* Type: STRING

* @see #COLUMN_FLAGS

* @see #FLAG_SUPPORTS_SEARCH

* @see #QUERY_ARG_DISPLAY_NAME

* @see #QUERY_ARG_FILE_SIZE_OVER

* @see #QUERY_ARG_LAST_MODIFIED_AFTER

* @see #QUERY_ARG_MIME_TYPES

* @see DocumentsProvider#querySearchDocuments(String, String[],

* Bundle)

*/

public static final String COLUMN_QUERY_ARGS = "query_args";

/**

* MIME type for a root.

*/

public static final String MIME_TYPE_ITEM = "vnd.android.document/root";

/**

* Flag indicating that at least one directory under this root supports

* creating content. Roots with this flag will be shown when an

* application interacts with {@link Intent#ACTION_CREATE_DOCUMENT}.

*

* @see #COLUMN_FLAGS

*/

public static final int FLAG_SUPPORTS_CREATE = 1;

/**

* Flag indicating that this root offers content that is strictly local

* on the device. That is, no network requests are made for the content.

*

* @see #COLUMN_FLAGS

* @see Intent#EXTRA_LOCAL_ONLY

*/

public static final int FLAG_LOCAL_ONLY = 1 << 1;

/**

* Flag indicating that this root can be queried to provide recently

* modified documents.

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#buildRecentDocumentsUri(String, String)

* @see DocumentsProvider#queryRecentDocuments(String, String[])

*/

public static final int FLAG_SUPPORTS_RECENTS = 1 << 2;

/**

* Flag indicating that this root supports search.

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#buildSearchDocumentsUri(String, String,

* String)

* @see DocumentsProvider#querySearchDocuments(String, String,

* String[])

* @see DocumentsProvider#querySearchDocuments(String, String[],

* Bundle)

*/

public static final int FLAG_SUPPORTS_SEARCH = 1 << 3;

/**

* Flag indicating that this root supports testing parent child

* relationships.

*

* @see #COLUMN_FLAGS

* @see DocumentsProvider#isChildDocument(String, String)

*/

public static final int FLAG_SUPPORTS_IS_CHILD = 1 << 4;

/**

* Flag indicating that this root can be ejected.

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#ejectRoot(ContentInterface, Uri)

* @see DocumentsProvider#ejectRoot(String)

*/

public static final int FLAG_SUPPORTS_EJECT = 1 << 5;

/**

* Flag indicating that this root is currently empty. This may be used

* to hide the root when opening documents, but the root will still be

* shown when creating documents and {@link #FLAG_SUPPORTS_CREATE} is

* also set. If the value of this flag changes, such as when a root

* becomes non-empty, you must send a content changed notification for

* {@link DocumentsContract#buildRootsUri(String)}.

*

* @see #COLUMN_FLAGS

* @see ContentResolver#notifyChange(Uri,

* android.database.ContentObserver, boolean)

*/

public static final int FLAG_EMPTY = 1 << 6;

/**

* Flag indicating that this root should only be visible to advanced

* users.

*

* @see #COLUMN_FLAGS

* {@hide}

*/

@SystemApi

public static final int FLAG_ADVANCED = 1 << 16;

/**

* Flag indicating that this root has settings.

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#ACTION_DOCUMENT_ROOT_SETTINGS

* {@hide}

*/

@SystemApi

public static final int FLAG_HAS_SETTINGS = 1 << 17;

/**

* Flag indicating that this root is on removable SD card storage.

*

* @see #COLUMN_FLAGS

* {@hide}

*/

@SystemApi

public static final int FLAG_REMOVABLE_SD = 1 << 18;

/**

* Flag indicating that this root is on removable USB storage.

*

* @see #COLUMN_FLAGS

* {@hide}

*/

@SystemApi

public static final int FLAG_REMOVABLE_USB = 1 << 19;

}

/**

* Optional boolean flag included in a directory {@link Cursor#getExtras()}

* indicating that a document provider is still loading data. For example, a

* provider has returned some results, but is still waiting on an

* outstanding network request. The provider must send a content changed

* notification when loading is finished.

*

* @see ContentResolver#notifyChange(Uri, android.database.ContentObserver,

* boolean)

*/

public static final String EXTRA_LOADING = "loading";

/**

* Optional string included in a directory {@link Cursor#getExtras()}

* providing an informational message that should be shown to a user. For

* example, a provider may wish to indicate that not all documents are

* available.

*/

public static final String EXTRA_INFO = "info";

/**

* Optional string included in a directory {@link Cursor#getExtras()}

* providing an error message that should be shown to a user. For example, a

* provider may wish to indicate that a network error occurred. The user may

* choose to retry, resulting in a new query.

*/

public static final String EXTRA_ERROR = "error";

/**

* Optional result (I'm thinking boolean) answer to a question.

* {@hide}

*/

public static final String EXTRA_RESULT = "result";

/** {@hide} */

@UnsupportedAppUsage

public static final String METHOD_CREATE_DOCUMENT = "android:createDocument";

/** {@hide} */

public static final String METHOD_RENAME_DOCUMENT = "android:renameDocument";

/** {@hide} */

public static final String METHOD_DELETE_DOCUMENT = "android:deleteDocument";

/** {@hide} */

public static final String METHOD_COPY_DOCUMENT = "android:copyDocument";

/** {@hide} */

public static final String METHOD_MOVE_DOCUMENT = "android:moveDocument";

/** {@hide} */

public static final String METHOD_IS_CHILD_DOCUMENT = "android:isChildDocument";

/** {@hide} */

public static final String METHOD_REMOVE_DOCUMENT = "android:removeDocument";

/** {@hide} */

public static final String METHOD_EJECT_ROOT = "android:ejectRoot";

/** {@hide} */

public static final String METHOD_FIND_DOCUMENT_PATH = "android:findDocumentPath";

/** {@hide} */

public static final String METHOD_CREATE_WEB_LINK_INTENT = "android:createWebLinkIntent";

/** {@hide} */

public static final String METHOD_GET_DOCUMENT_METADATA = "android:getDocumentMetadata";

/** {@hide} */

public static final String EXTRA_PARENT_URI = "parentUri";

/** {@hide} */

public static final String EXTRA_URI = "uri";

/** {@hide} */

public static final String EXTRA_URI_PERMISSIONS = "uriPermissions";

/** {@hide} */

public static final String EXTRA_OPTIONS = "options";

private static final String PATH_ROOT = "root";

private static final String PATH_RECENT = "recent";

@UnsupportedAppUsage

private static final String PATH_DOCUMENT = "document";

private static final String PATH_CHILDREN = "children";

private static final String PATH_SEARCH = "search";

@UnsupportedAppUsage

private static final String PATH_TREE = "tree";

private static final String PARAM_QUERY = "query";

private static final String PARAM_MANAGE = "manage";

/**

* Build URI representing the roots of a document provider. When queried, a

* provider will return one or more rows with columns defined by

* {@link Root}.

*

* @see DocumentsProvider#queryRoots(String[])

*/

public static Uri buildRootsUri(String authority) {

return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)

.authority(authority).appendPath(PATH_ROOT).build();

}

/**

* Build URI representing the given {@link Root#COLUMN_ROOT_ID} in a

* document provider.

*

* @see #getRootId(Uri)

*/

public static Uri buildRootUri(String authority, String rootId) {

return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)

.authority(authority).appendPath(PATH_ROOT).appendPath(rootId).build();

}

/**

* Build URI representing the recently modified documents of a specific root

* in a document provider. When queried, a provider will return zero or more

* rows with columns defined by {@link Document}.

*

* @see DocumentsProvider#queryRecentDocuments(String, String[])

* @see #getRootId(Uri)

*/

public static Uri buildRecentDocumentsUri(String authority, String rootId) {

return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)

.authority(authority).appendPath(PATH_ROOT).appendPath(rootId)

.appendPath(PATH_RECENT).build();

}

/**

* Build URI representing access to descendant documents of the given

* {@link Document#COLUMN_DOCUMENT_ID}.

*

* @see #getTreeDocumentId(Uri)

*/

public static Uri buildTreeDocumentUri(String authority, String documentId) {

return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)

.appendPath(PATH_TREE).appendPath(documentId).build();

}

/**

* Build URI representing the target {@link Document#COLUMN_DOCUMENT_ID} in

* a document provider. When queried, a provider will return a single row

* with columns defined by {@link Document}.

*

* @see DocumentsProvider#queryDocument(String, String[])

* @see #getDocumentId(Uri)

*/

public static Uri buildDocumentUri(String authority, String documentId) {

return getBaseDocumentUriBuilder(authority).appendPath(documentId).build();

}

/** {@hide} */

public static Uri buildBaseDocumentUri(String authority) {

return getBaseDocumentUriBuilder(authority).build();

}

private static Uri.Builder getBaseDocumentUriBuilder(String authority) {

return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)

.authority(authority).appendPath(PATH_DOCUMENT);

}

/**

* Build URI representing the target {@link Document#COLUMN_DOCUMENT_ID} in

* a document provider. When queried, a provider will return a single row

* with columns defined by {@link Document}.

*

* However, instead of directly accessing the target document, the returned

* URI will leverage access granted through a subtree URI, typically

* returned by {@link Intent#ACTION_OPEN_DOCUMENT_TREE}. The target document

* must be a descendant (child, grandchild, etc) of the subtree.

*

* This is typically used to access documents under a user-selected

* directory tree, since it doesn't require the user to separately confirm

* each new document access.

*

* @param treeUri the subtree to leverage to gain access to the target

* document. The target directory must be a descendant of this

* subtree.

* @param documentId the target document, which the caller may not have

* direct access to.

* @see Intent#ACTION_OPEN_DOCUMENT_TREE

* @see DocumentsProvider#isChildDocument(String, String)

* @see #buildDocumentUri(String, String)

*/

public static Uri buildDocumentUriUsingTree(Uri treeUri, String documentId) {

return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)

.authority(treeUri.getAuthority()).appendPath(PATH_TREE)

.appendPath(getTreeDocumentId(treeUri)).appendPath(PATH_DOCUMENT)

.appendPath(documentId).build();

}

/** {@hide} */

public static Uri buildDocumentUriMaybeUsingTree(Uri baseUri, String documentId) {

if (isTreeUri(baseUri)) {

return buildDocumentUriUsingTree(baseUri, documentId);

} else {

return buildDocumentUri(baseUri.getAuthority(), documentId);

}

}

/**

* Build URI representing the children of the target directory in a document

* provider. When queried, a provider will return zero or more rows with

* columns defined by {@link Document}.

*

* @param parentDocumentId the document to return children for, which must

* be a directory with MIME type of

* {@link Document#MIME_TYPE_DIR}.

* @see DocumentsProvider#queryChildDocuments(String, String[], String)

* @see #getDocumentId(Uri)

*/

public static Uri buildChildDocumentsUri(String authority, String parentDocumentId) {

return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)

.appendPath(PATH_DOCUMENT).appendPath(parentDocumentId).appendPath(PATH_CHILDREN)

.build();

}

/**

* Build URI representing the children of the target directory in a document

* provider. When queried, a provider will return zero or more rows with

* columns defined by {@link Document}.

*

* However, instead of directly accessing the target directory, the returned

* URI will leverage access granted through a subtree URI, typically

* returned by {@link Intent#ACTION_OPEN_DOCUMENT_TREE}. The target

* directory must be a descendant (child, grandchild, etc) of the subtree.

*

* This is typically used to access documents under a user-selected

* directory tree, since it doesn't require the user to separately confirm

* each new document access.

*

* @param treeUri the subtree to leverage to gain access to the target

* document. The target directory must be a descendant of this

* subtree.

* @param parentDocumentId the document to return children for, which the

* caller may not have direct access to, and which must be a

* directory with MIME type of {@link Document#MIME_TYPE_DIR}.

* @see Intent#ACTION_OPEN_DOCUMENT_TREE

* @see DocumentsProvider#isChildDocument(String, String)

* @see #buildChildDocumentsUri(String, String)

*/

public static Uri buildChildDocumentsUriUsingTree(Uri treeUri, String parentDocumentId) {

return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)

.authority(treeUri.getAuthority()).appendPath(PATH_TREE)

.appendPath(getTreeDocumentId(treeUri)).appendPath(PATH_DOCUMENT)

.appendPath(parentDocumentId).appendPath(PATH_CHILDREN).build();

}

/**

* Build URI representing a search for matching documents under a specific

* root in a document provider. When queried, a provider will return zero or

* more rows with columns defined by {@link Document}.

*

* @see DocumentsProvider#querySearchDocuments(String, String, String[])

* @see #getRootId(Uri)

* @see #getSearchDocumentsQuery(Uri)

*/

public static Uri buildSearchDocumentsUri(

String authority, String rootId, String query) {

return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)

.appendPath(PATH_ROOT).appendPath(rootId).appendPath(PATH_SEARCH)

.appendQueryParameter(PARAM_QUERY, query).build();

}

/**

* Check if the values match the query arguments.

*

* @param queryArgs the query arguments

* @param displayName the display time to check against

* @param mimeType the mime type to check against

* @param lastModified the last modified time to check against

* @param size the size to check against

* @hide

*/

public static boolean matchSearchQueryArguments(Bundle queryArgs, String displayName,

String mimeType, long lastModified, long size) {

if (queryArgs == null) {

return true;

}

final String argDisplayName = queryArgs.getString(QUERY_ARG_DISPLAY_NAME, "");

if (!argDisplayName.isEmpty()) {

// TODO (118795812) : Enhance the search string handled in DocumentsProvider

if (!displayName.toLowerCase().contains(argDisplayName.toLowerCase())) {

return false;

}

}

final long argFileSize = queryArgs.getLong(QUERY_ARG_FILE_SIZE_OVER, -1 /* defaultValue */);

if (argFileSize != -1 && size < argFileSize) {

return false;

}

final long argLastModified = queryArgs.getLong(QUERY_ARG_LAST_MODIFIED_AFTER,

-1 /* defaultValue */);

if (argLastModified != -1 && lastModified < argLastModified) {

return false;

}

final String[] argMimeTypes = queryArgs.getStringArray(QUERY_ARG_MIME_TYPES);

if (argMimeTypes != null && argMimeTypes.length > 0) {

mimeType = Intent.normalizeMimeType(mimeType);

for (String type : argMimeTypes) {

if (MimeTypeFilter.matches(mimeType, Intent.normalizeMimeType(type))) {

return true;

}

}

return false;

}

return true;

}

/**

* Get the handled query arguments from the query bundle. The handled arguments are

* {@link DocumentsContract#QUERY_ARG_EXCLUDE_MEDIA},

* {@link DocumentsContract#QUERY_ARG_DISPLAY_NAME},

* {@link DocumentsContract#QUERY_ARG_MIME_TYPES},

* {@link DocumentsContract#QUERY_ARG_FILE_SIZE_OVER} and

* {@link DocumentsContract#QUERY_ARG_LAST_MODIFIED_AFTER}.

*

* @param queryArgs the query arguments to be parsed.

* @return the handled query arguments

* @hide

*/

public static String[] getHandledQueryArguments(Bundle queryArgs) {

if (queryArgs == null) {

return new String[0];

}

final ArrayListargs = new ArrayList<>();

if (queryArgs.keySet().contains(QUERY_ARG_EXCLUDE_MEDIA)) {

args.add(QUERY_ARG_EXCLUDE_MEDIA);

}

if (queryArgs.keySet().contains(QUERY_ARG_DISPLAY_NAME)) {

args.add(QUERY_ARG_DISPLAY_NAME);

}

if (queryArgs.keySet().contains(QUERY_ARG_FILE_SIZE_OVER)) {

args.add(QUERY_ARG_FILE_SIZE_OVER);

}

if (queryArgs.keySet().contains(QUERY_ARG_LAST_MODIFIED_AFTER)) {

args.add(QUERY_ARG_LAST_MODIFIED_AFTER);

}

if (queryArgs.keySet().contains(QUERY_ARG_MIME_TYPES)) {

args.add(QUERY_ARG_MIME_TYPES);

}

return args.toArray(new String[0]);

}

/**

* Test if the given URI represents a {@link Document} backed by a

* {@link DocumentsProvider}.

*

* @see #buildDocumentUri(String, String)

* @see #buildDocumentUriUsingTree(Uri, String)

*/

public static boolean isDocumentUri(Context context, @Nullable Uri uri) {

if (isContentUri(uri) && isDocumentsProvider(context, uri.getAuthority())) {

final Listpaths = uri.getPathSegments();

if (paths.size() == 2) {

return PATH_DOCUMENT.equals(paths.get(0));

} else if (paths.size() == 4) {

return PATH_TREE.equals(paths.get(0)) && PATH_DOCUMENT.equals(paths.get(2));

}

}

return false;

}

/**

* Test if the given URI represents all roots of the authority

* backed by {@link DocumentsProvider}.

*

* @see #buildRootsUri(String)

*/

public static boolean isRootsUri(@NonNull Context context, @Nullable Uri uri) {

Preconditions.checkNotNull(context, "context can not be null");

return isRootUri(context, uri, 1 /* pathSize */);

}

/**

* Test if the given URI represents specific root backed by {@link DocumentsProvider}.

*

* @see #buildRootUri(String, String)

*/

public static boolean isRootUri(@NonNull Context context, @Nullable Uri uri) {

Preconditions.checkNotNull(context, "context can not be null");

return isRootUri(context, uri, 2 /* pathSize */);

}

/** {@hide} */

public static boolean isContentUri(@Nullable Uri uri) {

return uri != null && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme());

}

/**

* Test if the given URI represents a {@link Document} tree.

*

* @see #buildTreeDocumentUri(String, String)

* @see #getTreeDocumentId(Uri)

*/

public static boolean isTreeUri(Uri uri) {

final Listpaths = uri.getPathSegments();

return (paths.size() >= 2 && PATH_TREE.equals(paths.get(0)));

}

private static boolean isRootUri(Context context, @Nullable Uri uri, int pathSize) {

if (isContentUri(uri) && isDocumentsProvider(context, uri.getAuthority())) {

final Listpaths = uri.getPathSegments();

return (paths.size() == pathSize && PATH_ROOT.equals(paths.get(0)));

}

return false;

}

private static boolean isDocumentsProvider(Context context, String authority) {

final Intent intent = new Intent(PROVIDER_INTERFACE);

final Listinfos = context.getPackageManager()

.queryIntentContentProviders(intent, 0);

for (ResolveInfo info : infos) {

if (authority.equals(info.providerInfo.authority)) {

return true;

}

}

return false;

}

/**

* Extract the {@link Root#COLUMN_ROOT_ID} from the given URI.

*/

public static String getRootId(Uri rootUri) {

final Listpaths = rootUri.getPathSegments();

if (paths.size() >= 2 && PATH_ROOT.equals(paths.get(0))) {

return paths.get(1);

}

throw new IllegalArgumentException("Invalid URI: " + rootUri);

}

/**

* Extract the {@link Document#COLUMN_DOCUMENT_ID} from the given URI.

*

* @see #isDocumentUri(Context, Uri)

*/

public static String getDocumentId(Uri documentUri) {

final Listpaths = documentUri.getPathSegments();

if (paths.size() >= 2 && PATH_DOCUMENT.equals(paths.get(0))) {

return paths.get(1);

}

if (paths.size() >= 4 && PATH_TREE.equals(paths.get(0))

&& PATH_DOCUMENT.equals(paths.get(2))) {

return paths.get(3);

}

throw new IllegalArgumentException("Invalid URI: " + documentUri);

}

/**

* Extract the via {@link Document#COLUMN_DOCUMENT_ID} from the given URI.

*/

public static String getTreeDocumentId(Uri documentUri) {

final Listpaths = documentUri.getPathSegments();

if (paths.size() >= 2 && PATH_TREE.equals(paths.get(0))) {

return paths.get(1);

}

throw new IllegalArgumentException("Invalid URI: " + documentUri);

}

/**

* Extract the search query from a URI built by

* {@link #buildSearchDocumentsUri(String, String, String)}.

*/

public static String getSearchDocumentsQuery(Uri searchDocumentsUri) {

return searchDocumentsUri.getQueryParameter(PARAM_QUERY);

}

/**

* Extract the search query from a Bundle

* {@link #QUERY_ARG_DISPLAY_NAME}.

* {@hide}

*/

public static String getSearchDocumentsQuery(@NonNull Bundle bundle) {

Preconditions.checkNotNull(bundle, "bundle can not be null");

return bundle.getString(QUERY_ARG_DISPLAY_NAME, "" /* defaultValue */);

}

/**

* Build URI that append the query parameter {@link PARAM_MANAGE} to

* enable the manage mode.

* @see DocumentsProvider#queryChildDocumentsForManage(String parentDocId, String[], String)

* {@hide}

*/

@SystemApi

public static @NonNull Uri setManageMode(@NonNull Uri uri) {

Preconditions.checkNotNull(uri, "uri can not be null");

return uri.buildUpon().appendQueryParameter(PARAM_MANAGE, "true").build();

}

/**

* Extract the manage mode from a URI built by

* {@link #setManageMode(Uri)}.

* {@hide}

*/

@SystemApi

public static boolean isManageMode(@NonNull Uri uri) {

Preconditions.checkNotNull(uri, "uri can not be null");

return uri.getBooleanQueryParameter(PARAM_MANAGE, false);

}

/**

* Return thumbnail representing the document at the given URI. Callers are

* responsible for their own in-memory caching.

*

* @param documentUri document to return thumbnail for, which must have

* {@link Document#FLAG_SUPPORTS_THUMBNAIL} set.

* @param size optimal thumbnail size desired. A provider may return a

* thumbnail of a different size, but never more than double the

* requested size.

* @param signal signal used to indicate if caller is no longer interested

* in the thumbnail.

* @return decoded thumbnail, or {@code null} if problem was encountered.

* @see DocumentsProvider#openDocumentThumbnail(String, Point,

* android.os.CancellationSignal)

*/

public static @Nullable Bitmap getDocumentThumbnail(@NonNull ContentResolver content,

@NonNull Uri documentUri, @NonNull Point size, @Nullable CancellationSignal signal)

throws FileNotFoundException {

try {

return ContentResolver.loadThumbnail(content, documentUri, Point.convert(size), signal,

ImageDecoder.ALLOCATOR_SOFTWARE);

} catch (Exception e) {

if (!(e instanceof OperationCanceledException)) {

Log.w(TAG, "Failed to load thumbnail for " + documentUri + ": " + e);

}

rethrowIfNecessary(e);

return null;

}

}

/**

* Create a new document with given MIME type and display name.

*

* @param parentDocumentUri directory with {@link Document#FLAG_DIR_SUPPORTS_CREATE}

* @param mimeType MIME type of new document

* @param displayName name of new document

* @return newly created document, or {@code null} if failed

*/

public static @Nullable Uri createDocument(@NonNull ContentResolver content,

@NonNull Uri parentDocumentUri, @NonNull String mimeType, @NonNull String displayName)

throws FileNotFoundException {

try {

final Bundle in = new Bundle();

in.putParcelable(DocumentsContract.EXTRA_URI, parentDocumentUri);

in.putString(Document.COLUMN_MIME_TYPE, mimeType);

in.putString(Document.COLUMN_DISPLAY_NAME, displayName);

final Bundle out = content.call(parentDocumentUri.getAuthority(),

METHOD_CREATE_DOCUMENT, null, in);

return out.getParcelable(DocumentsContract.EXTRA_URI);

} catch (Exception e) {

Log.w(TAG, "Failed to create document", e);

rethrowIfNecessary(e);

return null;

}

}

/**

* Test if a document is descendant (child, grandchild, etc) from the given

* parent.

*

* @param parentDocumentUri parent to verify against.

* @param childDocumentUri child to verify.

* @return if given document is a descendant of the given parent.

* @see Root#FLAG_SUPPORTS_IS_CHILD

*/

public static boolean isChildDocument(@NonNull ContentResolver content,

@NonNull Uri parentDocumentUri, @NonNull Uri childDocumentUri)

throws FileNotFoundException {

Preconditions.checkNotNull(content, "content can not be null");

Preconditions.checkNotNull(parentDocumentUri, "parentDocumentUri can not be null");

Preconditions.checkNotNull(childDocumentUri, "childDocumentUri can not be null");

try {

final Bundle in = new Bundle();

in.putParcelable(DocumentsContract.EXTRA_URI, parentDocumentUri);

in.putParcelable(DocumentsContract.EXTRA_TARGET_URI, childDocumentUri);

final Bundle out = content.call(parentDocumentUri.getAuthority(),

METHOD_IS_CHILD_DOCUMENT, null, in);

if (out == null) {

throw new RemoteException("Failed to get a response from isChildDocument query.");

}

if (!out.containsKey(DocumentsContract.EXTRA_RESULT)) {

throw new RemoteException("Response did not include result field..");

}

return out.getBoolean(DocumentsContract.EXTRA_RESULT);

} catch (Exception e) {

Log.w(TAG, "Failed to create document", e);

rethrowIfNecessary(e);

return false;

}

}

/**

* Change the display name of an existing document.

*

* If the underlying provider needs to create a new

* {@link Document#COLUMN_DOCUMENT_ID} to represent the updated display

* name, that new document is returned and the original document is no

* longer valid. Otherwise, the original document is returned.

*

* @param documentUri document with {@link Document#FLAG_SUPPORTS_RENAME}

* @param displayName updated name for document

* @return the existing or new document after the rename, or {@code null} if

* failed.

*/

public static @Nullable Uri renameDocument(@NonNull ContentResolver content,

@NonNull Uri documentUri, @NonNull String displayName) throws FileNotFoundException {

try {

final Bundle in = new Bundle();

in.putParcelable(DocumentsContract.EXTRA_URI, documentUri);

in.putString(Document.COLUMN_DISPLAY_NAME, displayName);

final Bundle out = content.call(documentUri.getAuthority(),

METHOD_RENAME_DOCUMENT, null, in);

final Uri outUri = out.getParcelable(DocumentsContract.EXTRA_URI);

return (outUri != null) ? outUri : documentUri;

} catch (Exception e) {

Log.w(TAG, "Failed to rename document", e);

rethrowIfNecessary(e);

return null;

}

}

/**

* Delete the given document.

*

* @param documentUri document with {@link Document#FLAG_SUPPORTS_DELETE}

* @return if the document was deleted successfully.

*/

public static boolean deleteDocument(@NonNull ContentResolver content, @NonNull Uri documentUri)

throws FileNotFoundException {

try {

final Bundle in = new Bundle();

in.putParcelable(DocumentsContract.EXTRA_URI, documentUri);

content.call(documentUri.getAuthority(),

METHOD_DELETE_DOCUMENT, null, in);

return true;

} catch (Exception e) {

Log.w(TAG, "Failed to delete document", e);

rethrowIfNecessary(e);

return false;

}

}

/**

* Copies the given document.

*

* @param sourceDocumentUri document with {@link Document#FLAG_SUPPORTS_COPY}

* @param targetParentDocumentUri document which will become a parent of the source

* document's copy.

* @return the copied document, or {@code null} if failed.

*/

public static @Nullable Uri copyDocument(@NonNull ContentResolver content,

@NonNull Uri sourceDocumentUri, @NonNull Uri targetParentDocumentUri)

throws FileNotFoundException {

try {

final Bundle in = new Bundle();

in.putParcelable(DocumentsContract.EXTRA_URI, sourceDocumentUri);

in.putParcelable(DocumentsContract.EXTRA_TARGET_URI, targetParentDocumentUri);

final Bundle out = content.call(sourceDocumentUri.getAuthority(),

METHOD_COPY_DOCUMENT, null, in);

return out.getParcelable(DocumentsContract.EXTRA_URI);

} catch (Exception e) {

Log.w(TAG, "Failed to copy document", e);

rethrowIfNecessary(e);

return null;

}

}

/**

* Moves the given document under a new parent.

*

* @param sourceDocumentUri document with {@link Document#FLAG_SUPPORTS_MOVE}

* @param sourceParentDocumentUri parent document of the document to move.

* @param targetParentDocumentUri document which will become a new parent of the source

* document.

* @return the moved document, or {@code null} if failed.

*/

public static @Nullable Uri moveDocument(@NonNull ContentResolver content,

@NonNull Uri sourceDocumentUri, @NonNull Uri sourceParentDocumentUri,

@NonNull Uri targetParentDocumentUri) throws FileNotFoundException {

try {

final Bundle in = new Bundle();

in.putParcelable(DocumentsContract.EXTRA_URI, sourceDocumentUri);

in.putParcelable(DocumentsContract.EXTRA_PARENT_URI, sourceParentDocumentUri);

in.putParcelable(DocumentsContract.EXTRA_TARGET_URI, targetParentDocumentUri);

final Bundle out = content.call(sourceDocumentUri.getAuthority(),

METHOD_MOVE_DOCUMENT, null, in);

return out.getParcelable(DocumentsContract.EXTRA_URI);

} catch (Exception e) {

Log.w(TAG, "Failed to move document", e);

rethrowIfNecessary(e);

return null;

}

}

/**

* Removes the given document from a parent directory.

*

*

In contrast to {@link #deleteDocument} it requires specifying the parent.

* This method is especially useful if the document can be in multiple parents.

*

* @param documentUri document with {@link Document#FLAG_SUPPORTS_REMOVE}

* @param parentDocumentUri parent document of the document to remove.

* @return true if the document was removed successfully.

*/

public static boolean removeDocument(@NonNull ContentResolver content, @NonNull Uri documentUri,

@NonNull Uri parentDocumentUri) throws FileNotFoundException {

try {

final Bundle in = new Bundle();

in.putParcelable(DocumentsContract.EXTRA_URI, documentUri);

in.putParcelable(DocumentsContract.EXTRA_PARENT_URI, parentDocumentUri);

content.call(documentUri.getAuthority(),

METHOD_REMOVE_DOCUMENT, null, in);

return true;

} catch (Exception e) {

Log.w(TAG, "Failed to remove document", e);

rethrowIfNecessary(e);

return false;

}

}

/**

* Ejects the given root. It throws {@link IllegalStateException} when ejection failed.

*

* @param rootUri root with {@link Root#FLAG_SUPPORTS_EJECT} to be ejected

*/

public static void ejectRoot(@NonNull ContentResolver content, @NonNull Uri rootUri) {

try {

final Bundle in = new Bundle();

in.putParcelable(DocumentsContract.EXTRA_URI, rootUri);

content.call(rootUri.getAuthority(),

METHOD_EJECT_ROOT, null, in);

} catch (Exception e) {

Log.w(TAG, "Failed to eject", e);

}

}

/**

* Returns metadata associated with the document. The type of metadata returned

* is specific to the document type. For example the data returned for an image

* file will likely consist primarily or solely of EXIF metadata.

*

*

The returned {@link Bundle} will contain zero or more entries depending

* on the type of data supported by the document provider.

*

**

A {@link DocumentsContract#METADATA_TYPES} containing a {@code String[]} value.

* The string array identifies the type or types of metadata returned. Each

* value in the can be used to access a {@link Bundle} of data

* containing that type of data.

*

An entry each for each type of returned metadata. Each set of metadata is

* itself represented as a bundle and accessible via a string key naming

* the type of data.

*

*

*

Example:

*

* Bundle metadata = DocumentsContract.getDocumentMetadata(client, imageDocUri, tags);

* if (metadata.containsKey(DocumentsContract.METADATA_EXIF)) {

* Bundle exif = metadata.getBundle(DocumentsContract.METADATA_EXIF);

* int imageLength = exif.getInt(ExifInterface.TAG_IMAGE_LENGTH);

* }

*

*

* @param documentUri a Document URI

* @return a Bundle of Bundles.

*/

public static @Nullable Bundle getDocumentMetadata(@NonNull ContentResolver content,

@NonNull Uri documentUri) throws FileNotFoundException {

Preconditions.checkNotNull(content, "content can not be null");

Preconditions.checkNotNull(documentUri, "documentUri can not be null");

try {

final Bundle in = new Bundle();

in.putParcelable(EXTRA_URI, documentUri);

return content.call(documentUri.getAuthority(),

METHOD_GET_DOCUMENT_METADATA, null, in);

} catch (Exception e) {

Log.w(TAG, "Failed to get document metadata");

rethrowIfNecessary(e);

return null;

}

}

/**

* Finds the canonical path from the top of the document tree.

*

* The {@link Path#getPath()} of the return value contains the document ID

* of all documents along the path from the top the document tree to the

* requested document, both inclusive.

*

* The {@link Path#getRootId()} of the return value returns {@code null}.

*

* @param treeUri treeUri of the document which path is requested.

* @return the path of the document, or {@code null} if failed.

* @see DocumentsProvider#findDocumentPath(String, String)

*/

public static @Nullable Path findDocumentPath(@NonNull ContentResolver content,

@NonNull Uri treeUri) throws FileNotFoundException {

try {

final Bundle in = new Bundle();

in.putParcelable(DocumentsContract.EXTRA_URI, treeUri);

final Bundle out = content.call(treeUri.getAuthority(),

METHOD_FIND_DOCUMENT_PATH, null, in);

return out.getParcelable(DocumentsContract.EXTRA_RESULT);

} catch (Exception e) {

Log.w(TAG, "Failed to find path", e);

rethrowIfNecessary(e);

return null;

}

}

/**

* Creates an intent for obtaining a web link for the specified document.

*

*

Note, that due to internal limitations, if there is already a web link

* intent created for the specified document but with different options,

* then it may be overridden.

*

*

Providers are required to show confirmation UI for all new permissions granted

* for the linked document.

*

*

If list of recipients is known, then it should be passed in options as

* {@link Intent#EXTRA_EMAIL} as a list of email addresses. Note, that

* this is just a hint for the provider, which can ignore the list. In either

* case the provider is required to show a UI for letting the user confirm

* any new permission grants.

*

*

Note, that the entire options bundle will be sent to the provider

* backing the passed uri. Make sure that you trust the provider

* before passing any sensitive information.

*

*

Since this API may show a UI, it cannot be called from background.

*

*

In order to obtain the Web Link use code like this:

** void onSomethingHappened() {

* IntentSender sender = DocumentsContract.createWebLinkIntent(...);

* if (sender != null) {

* startIntentSenderForResult(

* sender,

* WEB_LINK_REQUEST_CODE,

* null, 0, 0, 0, null);

* }

* }

*

* (...)

*

* void onActivityResult(int requestCode, int resultCode, Intent data) {

* if (requestCode == WEB_LINK_REQUEST_CODE && resultCode == RESULT_OK) {

* Uri weblinkUri = data.getData();

* ...

* }

* }

*

*

* @param uri uri for the document to create a link to.

* @param options Extra information for generating the link.

* @return an intent sender to obtain the web link, or null if the document

* is not linkable, or creating the intent sender failed.

* @see DocumentsProvider#createWebLinkIntent(String, Bundle)

* @see Intent#EXTRA_EMAIL

*/

public static @Nullable IntentSender createWebLinkIntent(@NonNull ContentResolver content,

@NonNull Uri uri, @Nullable Bundle options) throws FileNotFoundException {

try {

final Bundle in = new Bundle();

in.putParcelable(DocumentsContract.EXTRA_URI, uri);

// Options may be provider specific, so put them in a separate bundle to

// avoid overriding the Uri.

if (options != null) {

in.putBundle(EXTRA_OPTIONS, options);

}

final Bundle out = content.call(uri.getAuthority(),

METHOD_CREATE_WEB_LINK_INTENT, null, in);

return out.getParcelable(DocumentsContract.EXTRA_RESULT);

} catch (Exception e) {

Log.w(TAG, "Failed to create a web link intent", e);

rethrowIfNecessary(e);

return null;

}

}

/**

* Open the given image for thumbnail purposes, using any embedded EXIF

* thumbnail if available, and providing orientation hints from the parent

* image.

*

* @hide

*/

public static AssetFileDescriptor openImageThumbnail(File file) throws FileNotFoundException {

final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(

file, ParcelFileDescriptor.MODE_READ_ONLY);

try {

final ExifInterface exif = new ExifInterface(file.getAbsolutePath());

final long[] thumb = exif.getThumbnailRange();

if (thumb != null) {

// If we use thumb to decode, we need to handle the rotation by ourselves.

Bundle extras = null;

switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1)) {

case ExifInterface.ORIENTATION_ROTATE_90:

extras = new Bundle(1);

extras.putInt(EXTRA_ORIENTATION, 90);

break;

case ExifInterface.ORIENTATION_ROTATE_180:

extras = new Bundle(1);

extras.putInt(EXTRA_ORIENTATION, 180);

break;

case ExifInterface.ORIENTATION_ROTATE_270:

extras = new Bundle(1);

extras.putInt(EXTRA_ORIENTATION, 270);

break;

}

return new AssetFileDescriptor(pfd, thumb[0], thumb[1], extras);

}

} catch (IOException e) {

}

// Do full file decoding, we don't need to handle the orientation

return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH, null);

}

private static void rethrowIfNecessary(Exception e) throws FileNotFoundException {

// We only want to throw applications targetting O and above

if (VMRuntime.getRuntime().getTargetSdkVersion() >= Build.VERSION_CODES.O) {

if (e instanceof ParcelableException) {

((ParcelableException) e).maybeRethrow(FileNotFoundException.class);

} else if (e instanceof RemoteException) {

((RemoteException) e).rethrowAsRuntimeException();

} else if (e instanceof RuntimeException) {

throw (RuntimeException) e;

}

}

}

/**

* Holds a path from a document to a particular document under it. It

* may also contains the root ID where the path resides.

*/

public static final class Path implements Parcelable {

private final @Nullable String mRootId;

private final ListmPath;

/**

* Creates a Path.

*

* @param rootId the ID of the root. May be null.

* @param path the list of document ID from the parent document at

* position 0 to the child document.

*/

public Path(@Nullable String rootId, Listpath) {

checkCollectionNotEmpty(path, "path");

checkCollectionElementsNotNull(path, "path");

mRootId = rootId;

mPath = path;

}

/**

* Returns the root id or null if the calling package doesn't have

* permission to access root information.

*/

public @Nullable String getRootId() {

return mRootId;

}

/**

* Returns the path. The path is trimmed to the top of tree if

* calling package doesn't have permission to access those

* documents.

*/

public ListgetPath() {

return mPath;

}

@Override

public boolean equals(Object o) {

if (this == o) {

return true;

}

if (o == null || !(o instanceof Path)) {

return false;

}

Path path = (Path) o;

return Objects.equals(mRootId, path.mRootId) &&

Objects.equals(mPath, path.mPath);

}

@Override

public int hashCode() {

return Objects.hash(mRootId, mPath);

}

@Override

public String toString() {

return new StringBuilder()

.append("DocumentsContract.Path{")

.append("rootId=")

.append(mRootId)

.append(", path=")

.append(mPath)

.append("}")

.toString();

}

@Override

public void writeToParcel(Parcel dest, int flags) {

dest.writeString(mRootId);

dest.writeStringList(mPath);

}

@Override

public int describeContents() {

return 0;

}

public static final @android.annotation.NonNull CreatorCREATOR = new Creator() {

@Override

public Path createFromParcel(Parcel in) {

final String rootId = in.readString();

final Listpath = in.createStringArrayList();

return new Path(rootId, path);

}

@Override

public Path[] newArray(int size) {

return new Path[size];

}

};

}

}

Java程序

|

1824行

|

70.33 KB

/*

* Copyright (C) 2013 The Android Open Source Project

*

* Licensed under the Apache License, Version 2.0 (the "License");

* you may not use this file except in compliance with the License.

* You may obtain a copy of the License at

*

* http://www.apache.org/licenses/LICENSE-2.0

*

* Unless required by applicable law or agreed to in writing, software

* distributed under the License is distributed on an "AS IS" BASIS,

* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

* See the License for the specific language governing permissions and

* limitations under the License.

*/

package android.provider;

import static com.android.internal.util.Preconditions.checkCollectionElementsNotNull;

import static com.android.internal.util.Preconditions.checkCollectionNotEmpty;

import android.annotation.NonNull;

import android.annotation.Nullable;

import android.annotation.SystemApi;

import android.annotation.UnsupportedAppUsage;

import android.content.ContentInterface;

import android.content.ContentResolver;

import android.content.Context;

import android.content.Intent;

import android.content.IntentSender;

import android.content.MimeTypeFilter;

import android.content.pm.ResolveInfo;

import android.content.res.AssetFileDescriptor;

import android.database.Cursor;

import android.graphics.Bitmap;

import android.graphics.ImageDecoder;

import android.graphics.Point;

import android.media.ExifInterface;

import android.net.Uri;

import android.os.Build;

import android.os.Bundle;

import android.os.CancellationSignal;

import android.os.OperationCanceledException;

import android.os.Parcel;

import android.os.ParcelFileDescriptor;

import android.os.ParcelFileDescriptor.OnCloseListener;

import android.os.Parcelable;

import android.os.ParcelableException;

import android.os.RemoteException;

import android.util.Log;

import com.android.internal.util.Preconditions;

import dalvik.system.VMRuntime;

import java.io.File;

import java.io.FileNotFoundException;

import java.io.IOException;

import java.util.ArrayList;

import java.util.List;

import java.util.Objects;

/**

* Defines the contract between a documents provider and the platform.

*

* To create a document provider, extend {@link DocumentsProvider}, which

* provides a foundational implementation of this contract.

*

* All client apps must hold a valid URI permission grant to access documents,

* typically issued when a user makes a selection through

* {@link Intent#ACTION_OPEN_DOCUMENT}, {@link Intent#ACTION_CREATE_DOCUMENT},

* or {@link Intent#ACTION_OPEN_DOCUMENT_TREE}.

*

* @see DocumentsProvider

*/

public final class DocumentsContract {

private static final String TAG = "DocumentsContract";

// content://com.example/root/

// content://com.example/root/sdcard/

// content://com.example/root/sdcard/recent/

// content://com.example/root/sdcard/search/?query=pony

// content://com.example/document/12/

// content://com.example/document/12/children/

// content://com.example/tree/12/document/24/

// content://com.example/tree/12/document/24/children/

private DocumentsContract() {

}

/**

* Intent action used to identify {@link DocumentsProvider} instances. This

* is used in the {@code } of a {@code }.

*/

public static final String PROVIDER_INTERFACE = "android.content.action.DOCUMENTS_PROVIDER";

/** {@hide} */

@Deprecated

public static final String EXTRA_PACKAGE_NAME = Intent.EXTRA_PACKAGE_NAME;

/**

* The value is decide whether to show advance mode or not.

* If the value is true, the local/device storage root must be

* visible in DocumentsUI.

*

* {@hide}

*/

@SystemApi

public static final String EXTRA_SHOW_ADVANCED = "android.provider.extra.SHOW_ADVANCED";

/** {@hide} */

public static final String EXTRA_TARGET_URI = "android.content.extra.TARGET_URI";

/**

* Key for {@link DocumentsProvider} to query display name is matched.

* The match of display name is partial matching and case-insensitive.

* Ex: The value is "o", the display name of the results will contain

* both "foo" and "Open".

*

* @see DocumentsProvider#querySearchDocuments(String, String[],

* Bundle)

*/

public static final String QUERY_ARG_DISPLAY_NAME = "android:query-arg-display-name";

/**

* Key for {@link DocumentsProvider} to query mime types is matched.

* The value is a string array, it can support different mime types.

* Each items will be treated as "OR" condition. Ex: {"image/*" ,

* "video/*"}. The mime types of the results will contain both image

* type and video type.

*

* @see DocumentsProvider#querySearchDocuments(String, String[],

* Bundle)

*/

public static final String QUERY_ARG_MIME_TYPES = "android:query-arg-mime-types";

/**

* Key for {@link DocumentsProvider} to query the file size in bytes is

* larger than the value.

*

* @see DocumentsProvider#querySearchDocuments(String, String[],

* Bundle)

*/

public static final String QUERY_ARG_FILE_SIZE_OVER = "android:query-arg-file-size-over";

/**

* Key for {@link DocumentsProvider} to query the last modified time

* is newer than the value. The unit is in milliseconds since

* January 1, 1970 00:00:00.0 UTC.

*

* @see DocumentsProvider#querySearchDocuments(String, String[],

* Bundle)

* @see Document#COLUMN_LAST_MODIFIED

*/

public static final String QUERY_ARG_LAST_MODIFIED_AFTER =

"android:query-arg-last-modified-after";

/**

* Key for {@link DocumentsProvider} to decide whether the files that

* have been added to MediaStore should be excluded. If the value is

* true, exclude them. Otherwise, include them.

*

* @see DocumentsProvider#querySearchDocuments(String, String[],

* Bundle)

*/

public static final String QUERY_ARG_EXCLUDE_MEDIA = "android:query-arg-exclude-media";

/**

* Sets the desired initial location visible to user when file chooser is shown.

*

*

Applicable to {@link Intent} with actions:

*

*

{@link Intent#ACTION_OPEN_DOCUMENT}

*

{@link Intent#ACTION_CREATE_DOCUMENT}

*

{@link Intent#ACTION_OPEN_DOCUMENT_TREE}

*

*

*

Location should specify a document URI or a tree URI with document ID. If

* this URI identifies a non-directory, document navigator will attempt to use the parent

* of the document as the initial location.

*

*

The initial location is system specific if this extra is missing or document navigator

* failed to locate the desired initial location.

*/

public static final String EXTRA_INITIAL_URI = "android.provider.extra.INITIAL_URI";

/**

* Set this in a DocumentsUI intent to cause a package's own roots to be

* excluded from the roots list.

*/

public static final String EXTRA_EXCLUDE_SELF = "android.provider.extra.EXCLUDE_SELF";

/**

* An extra number of degrees that an image should be rotated during the

* decode process to be presented correctly.

*

* @see AssetFileDescriptor#getExtras()

* @see android.provider.MediaStore.Images.ImageColumns#ORIENTATION

*/

public static final String EXTRA_ORIENTATION = "android.provider.extra.ORIENTATION";

/**

* Overrides the default prompt text in DocumentsUI when set in an intent.

*/

public static final String EXTRA_PROMPT = "android.provider.extra.PROMPT";

/**

* Action of intent issued by DocumentsUI when user wishes to open/configure/manage a particular

* document in the provider application.

*

*

When issued, the intent will include the URI of the document as the intent data.

*

*

A provider wishing to provide support for this action should do two things.

*

Add an {@code } matching this action.

*

When supplying information in {@link DocumentsProvider#queryChildDocuments}, include

* {@link Document#FLAG_SUPPORTS_SETTINGS} in the flags for each document that supports

* settings.

*/

public static final String

ACTION_DOCUMENT_SETTINGS = "android.provider.action.DOCUMENT_SETTINGS";

/**

* The action to manage document in Downloads root in DocumentsUI.

* {@hide}

*/

@SystemApi

public static final String ACTION_MANAGE_DOCUMENT = "android.provider.action.MANAGE_DOCUMENT";

/**

* The action to launch the settings of this root.

* {@hide}

*/

@SystemApi

public static final String

ACTION_DOCUMENT_ROOT_SETTINGS = "android.provider.action.DOCUMENT_ROOT_SETTINGS";

/** {@hide} */

public static final String EXTERNAL_STORAGE_PROVIDER_AUTHORITY =

"com.android.externalstorage.documents";

/** {@hide} */

public static final String EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID = "primary";

/** {@hide} */

public static final String PACKAGE_DOCUMENTS_UI = "com.android.documentsui";

/**

* Get string array identifies the type or types of metadata returned

* using DocumentsContract#getDocumentMetadata.

*

* @see #getDocumentMetadata(ContentInterface, Uri)

*/

public static final String METADATA_TYPES = "android:documentMetadataTypes";

/**

* Get Exif information using DocumentsContract#getDocumentMetadata.

*

* @see #getDocumentMetadata(ContentInterface, Uri)

*/

public static final String METADATA_EXIF = "android:documentExif";

/**

* Get total count of all documents currently stored under the given

* directory tree. Only valid for {@link Document#MIME_TYPE_DIR} documents.

*

* @see #getDocumentMetadata(ContentInterface, Uri)

*/

public static final String METADATA_TREE_COUNT = "android:metadataTreeCount";

/**

* Get total size of all documents currently stored under the given

* directory tree. Only valid for {@link Document#MIME_TYPE_DIR} documents.

*

* @see #getDocumentMetadata(ContentInterface, Uri)

*/

public static final String METADATA_TREE_SIZE = "android:metadataTreeSize";

/**

* Constants related to a document, including {@link Cursor} column names

* and flags.

*

* A document can be either an openable stream (with a specific MIME type),

* or a directory containing additional documents (with the

* {@link #MIME_TYPE_DIR} MIME type). A directory represents the top of a

* subtree containing zero or more documents, which can recursively contain

* even more documents and directories.

*

* All columns are read-only to client applications.

*/

public final static class Document {

private Document() {

}

/**

* Unique ID of a document. This ID is both provided by and interpreted

* by a {@link DocumentsProvider}, and should be treated as an opaque

* value by client applications. This column is required.

*

* Each document must have a unique ID within a provider, but that

* single document may be included as a child of multiple directories.

*

* A provider must always return durable IDs, since they will be used to

* issue long-term URI permission grants when an application interacts

* with {@link Intent#ACTION_OPEN_DOCUMENT} and

* {@link Intent#ACTION_CREATE_DOCUMENT}.

*

* Type: STRING

*/

public static final String COLUMN_DOCUMENT_ID = "document_id";

/**

* Concrete MIME type of a document. For example, "image/png" or

* "application/pdf" for openable files. A document can also be a

* directory containing additional documents, which is represented with

* the {@link #MIME_TYPE_DIR} MIME type. This column is required.

*

* Type: STRING

*

* @see #MIME_TYPE_DIR

*/

public static final String COLUMN_MIME_TYPE = "mime_type";

/**

* Display name of a document, used as the primary title displayed to a

* user. This column is required.

*

* Type: STRING

*/

public static final String COLUMN_DISPLAY_NAME = OpenableColumns.DISPLAY_NAME;

/**

* Summary of a document, which may be shown to a user. This column is

* optional, and may be {@code null}.

*

* Type: STRING

*/

public static final String COLUMN_SUMMARY = "summary";

/**

* Timestamp when a document was last modified, in milliseconds since

* January 1, 1970 00:00:00.0 UTC. This column is required, and may be

* {@code null} if unknown. A {@link DocumentsProvider} can update this

* field using events from {@link OnCloseListener} or other reliable

* {@link ParcelFileDescriptor} transports.

*

* Type: INTEGER (long)

*

* @see System#currentTimeMillis()

*/

public static final String COLUMN_LAST_MODIFIED = "last_modified";

/**

* Specific icon resource ID for a document. This column is optional,

* and may be {@code null} to use a platform-provided default icon based

* on {@link #COLUMN_MIME_TYPE}.

*

* Type: INTEGER (int)

*/

public static final String COLUMN_ICON = "icon";

/**

* Flags that apply to a document. This column is required.

*

* Type: INTEGER (int)

*

* @see #FLAG_SUPPORTS_WRITE

* @see #FLAG_SUPPORTS_DELETE

* @see #FLAG_SUPPORTS_THUMBNAIL

* @see #FLAG_DIR_PREFERS_GRID

* @see #FLAG_DIR_PREFERS_LAST_MODIFIED

* @see #FLAG_VIRTUAL_DOCUMENT

* @see #FLAG_SUPPORTS_COPY

* @see #FLAG_SUPPORTS_MOVE

* @see #FLAG_SUPPORTS_REMOVE

*/

public static final String COLUMN_FLAGS = "flags";

/**

* Size of a document, in bytes, or {@code null} if unknown. This column

* is required.

*

* Type: INTEGER (long)

*/

public static final String COLUMN_SIZE = OpenableColumns.SIZE;

/**

* MIME type of a document which is a directory that may contain

* additional documents.

*

* @see #COLUMN_MIME_TYPE

*/

public static final String MIME_TYPE_DIR = "vnd.android.document/directory";

/**

* Flag indicating that a document can be represented as a thumbnail.

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#getDocumentThumbnail(ContentInterface, Uri,

* Point, CancellationSignal)

* @see DocumentsProvider#openDocumentThumbnail(String, Point,

* android.os.CancellationSignal)

*/

public static final int FLAG_SUPPORTS_THUMBNAIL = 1;

/**

* Flag indicating that a document supports writing.

*

* When a document is opened with {@link Intent#ACTION_OPEN_DOCUMENT},

* the calling application is granted both

* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and

* {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. However, the actual

* writability of a document may change over time, for example due to

* remote access changes. This flag indicates that a document client can

* expect {@link ContentResolver#openOutputStream(Uri)} to succeed.

*

* @see #COLUMN_FLAGS

*/

public static final int FLAG_SUPPORTS_WRITE = 1 << 1;

/**

* Flag indicating that a document is deletable.

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#deleteDocument(ContentInterface, Uri)

* @see DocumentsProvider#deleteDocument(String)

*/

public static final int FLAG_SUPPORTS_DELETE = 1 << 2;

/**

* Flag indicating that a document is a directory that supports creation

* of new files within it. Only valid when {@link #COLUMN_MIME_TYPE} is

* {@link #MIME_TYPE_DIR}.

*

* @see #COLUMN_FLAGS

* @see DocumentsProvider#createDocument(String, String, String)

*/

public static final int FLAG_DIR_SUPPORTS_CREATE = 1 << 3;

/**

* Flag indicating that a directory prefers its contents be shown in a

* larger format grid. Usually suitable when a directory contains mostly

* pictures. Only valid when {@link #COLUMN_MIME_TYPE} is

* {@link #MIME_TYPE_DIR}.

*

* @see #COLUMN_FLAGS

*/

public static final int FLAG_DIR_PREFERS_GRID = 1 << 4;

/**

* Flag indicating that a directory prefers its contents be sorted by

* {@link #COLUMN_LAST_MODIFIED}. Only valid when

* {@link #COLUMN_MIME_TYPE} is {@link #MIME_TYPE_DIR}.

*

* @see #COLUMN_FLAGS

*/

public static final int FLAG_DIR_PREFERS_LAST_MODIFIED = 1 << 5;

/**

* Flag indicating that a document can be renamed.

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#renameDocument(ContentInterface, Uri, String)

* @see DocumentsProvider#renameDocument(String, String)

*/

public static final int FLAG_SUPPORTS_RENAME = 1 << 6;

/**

* Flag indicating that a document can be copied to another location

* within the same document provider.

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#copyDocument(ContentInterface, Uri, Uri)

* @see DocumentsProvider#copyDocument(String, String)

*/

public static final int FLAG_SUPPORTS_COPY = 1 << 7;

/**

* Flag indicating that a document can be moved to another location

* within the same document provider.

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#moveDocument(ContentInterface, Uri, Uri, Uri)

* @see DocumentsProvider#moveDocument(String, String, String)

*/

public static final int FLAG_SUPPORTS_MOVE = 1 << 8;

/**

* Flag indicating that a document is virtual, and doesn't have byte

* representation in the MIME type specified as {@link #COLUMN_MIME_TYPE}.

*

*

Virtual documents must have at least one alternative streamable

* format via {@link DocumentsProvider#openTypedDocument}

*

* @see #COLUMN_FLAGS

* @see #COLUMN_MIME_TYPE

* @see DocumentsProvider#openTypedDocument(String, String, Bundle,

* android.os.CancellationSignal)

* @see DocumentsProvider#getDocumentStreamTypes(String, String)

*/

public static final int FLAG_VIRTUAL_DOCUMENT = 1 << 9;

/**

* Flag indicating that a document can be removed from a parent.

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#removeDocument(ContentInterface, Uri, Uri)

* @see DocumentsProvider#removeDocument(String, String)

*/

public static final int FLAG_SUPPORTS_REMOVE = 1 << 10;

/**

* Flag indicating that a document has settings that can be configured by user.

*

* @see #COLUMN_FLAGS

* @see #ACTION_DOCUMENT_SETTINGS

*/

public static final int FLAG_SUPPORTS_SETTINGS = 1 << 11;

/**

* Flag indicating that a Web link can be obtained for the document.

*

* @see #COLUMN_FLAGS

* @see DocumentsProvider#createWebLinkIntent(String, Bundle)

*/

public static final int FLAG_WEB_LINKABLE = 1 << 12;

/**

* Flag indicating that a document is not complete, likely its

* contents are being downloaded. Partial files cannot be opened,

* copied, moved in the UI. But they can be deleted and retried

* if they represent a failed download.

*

* @see #COLUMN_FLAGS

*/

public static final int FLAG_PARTIAL = 1 << 13;

/**

* Flag indicating that a document has available metadata that can be read

* using DocumentsContract#getDocumentMetadata

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#getDocumentMetadata(ContentInterface, Uri)

*/

public static final int FLAG_SUPPORTS_METADATA = 1 << 14;

}

/**

* Constants related to a root of documents, including {@link Cursor} column

* names and flags. A root is the start of a tree of documents, such as a

* physical storage device, or an account. Each root starts at the directory

* referenced by {@link Root#COLUMN_DOCUMENT_ID}, which can recursively

* contain both documents and directories.

*

* All columns are read-only to client applications.

*/

public final static class Root {

private Root() {

}

/**

* Unique ID of a root. This ID is both provided by and interpreted by a

* {@link DocumentsProvider}, and should be treated as an opaque value

* by client applications. This column is required.

*

* Type: STRING

*/

public static final String COLUMN_ROOT_ID = "root_id";

/**

* Flags that apply to a root. This column is required.

*

* Type: INTEGER (int)

*

* @see #FLAG_LOCAL_ONLY

* @see #FLAG_SUPPORTS_CREATE

* @see #FLAG_SUPPORTS_RECENTS

* @see #FLAG_SUPPORTS_SEARCH

*/

public static final String COLUMN_FLAGS = "flags";

/**

* Icon resource ID for a root. This column is required.

*

* Type: INTEGER (int)

*/

public static final String COLUMN_ICON = "icon";

/**

* Title for a root, which will be shown to a user. This column is

* required. For a single storage service surfacing multiple accounts as

* different roots, this title should be the name of the service.

*

* Type: STRING

*/

public static final String COLUMN_TITLE = "title";

/**

* Summary for this root, which may be shown to a user. This column is

* optional, and may be {@code null}. For a single storage service

* surfacing multiple accounts as different roots, this summary should

* be the name of the account.

*

* Type: STRING

*/

public static final String COLUMN_SUMMARY = "summary";

/**

* Document which is a directory that represents the top directory of

* this root. This column is required.

*

* Type: STRING

*

* @see Document#COLUMN_DOCUMENT_ID

*/

public static final String COLUMN_DOCUMENT_ID = "document_id";

/**

* Number of bytes available in this root. This column is optional, and

* may be {@code null} if unknown or unbounded.

*

* Type: INTEGER (long)

*/

public static final String COLUMN_AVAILABLE_BYTES = "available_bytes";

/**

* Capacity of a root in bytes. This column is optional, and may be

* {@code null} if unknown or unbounded.

*

* Type: INTEGER (long)

*/

public static final String COLUMN_CAPACITY_BYTES = "capacity_bytes";

/**

* MIME types supported by this root. This column is optional, and if

* {@code null} the root is assumed to support all MIME types. Multiple

* MIME types can be separated by a newline. For example, a root

* supporting audio might return "audio/*\napplication/x-flac".

*

* Type: STRING

*/

public static final String COLUMN_MIME_TYPES = "mime_types";

/**

* Query arguments supported by this root. This column is optional

* and related to {@link #COLUMN_FLAGS} and {@link #FLAG_SUPPORTS_SEARCH}.

* If the flags include {@link #FLAG_SUPPORTS_SEARCH}, and the column is

* {@code null}, the root is assumed to support {@link #QUERY_ARG_DISPLAY_NAME}

* search of {@link Document#COLUMN_DISPLAY_NAME}. Multiple query arguments

* can be separated by a newline. For example, a root supporting

* {@link #QUERY_ARG_MIME_TYPES} and {@link #QUERY_ARG_DISPLAY_NAME} might

* return "android:query-arg-mime-types\nandroid:query-arg-display-name".

*

* Type: STRING

* @see #COLUMN_FLAGS

* @see #FLAG_SUPPORTS_SEARCH

* @see #QUERY_ARG_DISPLAY_NAME

* @see #QUERY_ARG_FILE_SIZE_OVER

* @see #QUERY_ARG_LAST_MODIFIED_AFTER

* @see #QUERY_ARG_MIME_TYPES

* @see DocumentsProvider#querySearchDocuments(String, String[],

* Bundle)

*/

public static final String COLUMN_QUERY_ARGS = "query_args";

/**

* MIME type for a root.

*/

public static final String MIME_TYPE_ITEM = "vnd.android.document/root";

/**

* Flag indicating that at least one directory under this root supports

* creating content. Roots with this flag will be shown when an

* application interacts with {@link Intent#ACTION_CREATE_DOCUMENT}.

*

* @see #COLUMN_FLAGS

*/

public static final int FLAG_SUPPORTS_CREATE = 1;

/**

* Flag indicating that this root offers content that is strictly local

* on the device. That is, no network requests are made for the content.

*

* @see #COLUMN_FLAGS

* @see Intent#EXTRA_LOCAL_ONLY

*/

public static final int FLAG_LOCAL_ONLY = 1 << 1;

/**

* Flag indicating that this root can be queried to provide recently

* modified documents.

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#buildRecentDocumentsUri(String, String)

* @see DocumentsProvider#queryRecentDocuments(String, String[])

*/

public static final int FLAG_SUPPORTS_RECENTS = 1 << 2;

/**

* Flag indicating that this root supports search.

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#buildSearchDocumentsUri(String, String,

* String)

* @see DocumentsProvider#querySearchDocuments(String, String,

* String[])

* @see DocumentsProvider#querySearchDocuments(String, String[],

* Bundle)

*/

public static final int FLAG_SUPPORTS_SEARCH = 1 << 3;

/**

* Flag indicating that this root supports testing parent child

* relationships.

*

* @see #COLUMN_FLAGS

* @see DocumentsProvider#isChildDocument(String, String)

*/

public static final int FLAG_SUPPORTS_IS_CHILD = 1 << 4;

/**

* Flag indicating that this root can be ejected.

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#ejectRoot(ContentInterface, Uri)

* @see DocumentsProvider#ejectRoot(String)

*/

public static final int FLAG_SUPPORTS_EJECT = 1 << 5;

/**

* Flag indicating that this root is currently empty. This may be used

* to hide the root when opening documents, but the root will still be

* shown when creating documents and {@link #FLAG_SUPPORTS_CREATE} is

* also set. If the value of this flag changes, such as when a root

* becomes non-empty, you must send a content changed notification for

* {@link DocumentsContract#buildRootsUri(String)}.

*

* @see #COLUMN_FLAGS

* @see ContentResolver#notifyChange(Uri,

* android.database.ContentObserver, boolean)

*/

public static final int FLAG_EMPTY = 1 << 6;

/**

* Flag indicating that this root should only be visible to advanced

* users.

*

* @see #COLUMN_FLAGS

* {@hide}

*/

@SystemApi

public static final int FLAG_ADVANCED = 1 << 16;

/**

* Flag indicating that this root has settings.

*

* @see #COLUMN_FLAGS

* @see DocumentsContract#ACTION_DOCUMENT_ROOT_SETTINGS

* {@hide}

*/

@SystemApi

public static final int FLAG_HAS_SETTINGS = 1 << 17;

/**

* Flag indicating that this root is on removable SD card storage.

*

* @see #COLUMN_FLAGS

* {@hide}

*/

@SystemApi

public static final int FLAG_REMOVABLE_SD = 1 << 18;

/**

* Flag indicating that this root is on removable USB storage.

*

* @see #COLUMN_FLAGS

* {@hide}

*/

@SystemApi

public static final int FLAG_REMOVABLE_USB = 1 << 19;

}

/**

* Optional boolean flag included in a directory {@link Cursor#getExtras()}

* indicating that a document provider is still loading data. For example, a

* provider has returned some results, but is still waiting on an

* outstanding network request. The provider must send a content changed

* notification when loading is finished.

*

* @see ContentResolver#notifyChange(Uri, android.database.ContentObserver,

* boolean)

*/

public static final String EXTRA_LOADING = "loading";

/**

* Optional string included in a directory {@link Cursor#getExtras()}

* providing an informational message that should be shown to a user. For

* example, a provider may wish to indicate that not all documents are

* available.

*/

public static final String EXTRA_INFO = "info";

/**

* Optional string included in a directory {@link Cursor#getExtras()}

* providing an error message that should be shown to a user. For example, a

* provider may wish to indicate that a network error occurred. The user may

* choose to retry, resulting in a new query.

*/

public static final String EXTRA_ERROR = "error";

/**

* Optional result (I'm thinking boolean) answer to a question.

* {@hide}

*/

public static final String EXTRA_RESULT = "result";

/** {@hide} */

@UnsupportedAppUsage

public static final String METHOD_CREATE_DOCUMENT = "android:createDocument";

/** {@hide} */

public static final String METHOD_RENAME_DOCUMENT = "android:renameDocument";

/** {@hide} */

public static final String METHOD_DELETE_DOCUMENT = "android:deleteDocument";

/** {@hide} */

public static final String METHOD_COPY_DOCUMENT = "android:copyDocument";

/** {@hide} */

public static final String METHOD_MOVE_DOCUMENT = "android:moveDocument";

/** {@hide} */

public static final String METHOD_IS_CHILD_DOCUMENT = "android:isChildDocument";

/** {@hide} */

public static final String METHOD_REMOVE_DOCUMENT = "android:removeDocument";

/** {@hide} */

public static final String METHOD_EJECT_ROOT = "android:ejectRoot";

/** {@hide} */

public static final String METHOD_FIND_DOCUMENT_PATH = "android:findDocumentPath";

/** {@hide} */

public static final String METHOD_CREATE_WEB_LINK_INTENT = "android:createWebLinkIntent";

/** {@hide} */

public static final String METHOD_GET_DOCUMENT_METADATA = "android:getDocumentMetadata";

/** {@hide} */

public static final String EXTRA_PARENT_URI = "parentUri";

/** {@hide} */

public static final String EXTRA_URI = "uri";

/** {@hide} */

public static final String EXTRA_URI_PERMISSIONS = "uriPermissions";

/** {@hide} */

public static final String EXTRA_OPTIONS = "options";

private static final String PATH_ROOT = "root";

private static final String PATH_RECENT = "recent";

@UnsupportedAppUsage

private static final String PATH_DOCUMENT = "document";

private static final String PATH_CHILDREN = "children";

private static final String PATH_SEARCH = "search";

@UnsupportedAppUsage

private static final String PATH_TREE = "tree";

private static final String PARAM_QUERY = "query";

private static final String PARAM_MANAGE = "manage";

/**

* Build URI representing the roots of a document provider. When queried, a

* provider will return one or more rows with columns defined by

* {@link Root}.

*

* @see DocumentsProvider#queryRoots(String[])

*/

public static Uri buildRootsUri(String authority) {

return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)

.authority(authority).appendPath(PATH_ROOT).build();

}

/**

* Build URI representing the given {@link Root#COLUMN_ROOT_ID} in a

* document provider.

*

* @see #getRootId(Uri)

*/

public static Uri buildRootUri(String authority, String rootId) {

return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)

.authority(authority).appendPath(PATH_ROOT).appendPath(rootId).build();

}

/**

* Build URI representing the recently modified documents of a specific root

* in a document provider. When queried, a provider will return zero or more

* rows with columns defined by {@link Document}.

*

* @see DocumentsProvider#queryRecentDocuments(String, String[])

* @see #getRootId(Uri)

*/

public static Uri buildRecentDocumentsUri(String authority, String rootId) {

return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)

.authority(authority).appendPath(PATH_ROOT).appendPath(rootId)

.appendPath(PATH_RECENT).build();

}

/**

* Build URI representing access to descendant documents of the given

* {@link Document#COLUMN_DOCUMENT_ID}.

*

* @see #getTreeDocumentId(Uri)

*/

public static Uri buildTreeDocumentUri(String authority, String documentId) {

return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)

.appendPath(PATH_TREE).appendPath(documentId).build();

}

/**

* Build URI representing the target {@link Document#COLUMN_DOCUMENT_ID} in

* a document provider. When queried, a provider will return a single row

* with columns defined by {@link Document}.

*

* @see DocumentsProvider#queryDocument(String, String[])

* @see #getDocumentId(Uri)

*/

public static Uri buildDocumentUri(String authority, String documentId) {

return getBaseDocumentUriBuilder(authority).appendPath(documentId).build();

}

/** {@hide} */

public static Uri buildBaseDocumentUri(String authority) {

return getBaseDocumentUriBuilder(authority).build();

}

private static Uri.Builder getBaseDocumentUriBuilder(String authority) {

return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)

.authority(authority).appendPath(PATH_DOCUMENT);

}

/**

* Build URI representing the target {@link Document#COLUMN_DOCUMENT_ID} in

* a document provider. When queried, a provider will return a single row

* with columns defined by {@link Document}.

*

* However, instead of directly accessing the target document, the returned

* URI will leverage access granted through a subtree URI, typically

* returned by {@link Intent#ACTION_OPEN_DOCUMENT_TREE}. The target document

* must be a descendant (child, grandchild, etc) of the subtree.

*

* This is typically used to access documents under a user-selected

* directory tree, since it doesn't require the user to separately confirm

* each new document access.

*

* @param treeUri the subtree to leverage to gain access to the target

* document. The target directory must be a descendant of this

* subtree.

* @param documentId the target document, which the caller may not have

* direct access to.

* @see Intent#ACTION_OPEN_DOCUMENT_TREE

* @see DocumentsProvider#isChildDocument(String, String)

* @see #buildDocumentUri(String, String)

*/

public static Uri buildDocumentUriUsingTree(Uri treeUri, String documentId) {

return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)

.authority(treeUri.getAuthority()).appendPath(PATH_TREE)

.appendPath(getTreeDocumentId(treeUri)).appendPath(PATH_DOCUMENT)

.appendPath(documentId).build();

}

/** {@hide} */

public static Uri buildDocumentUriMaybeUsingTree(Uri baseUri, String documentId) {

if (isTreeUri(baseUri)) {

return buildDocumentUriUsingTree(baseUri, documentId);

} else {

return buildDocumentUri(baseUri.getAuthority(), documentId);

}

}

/**

* Build URI representing the children of the target directory in a document

* provider. When queried, a provider will return zero or more rows with

* columns defined by {@link Document}.

*

* @param parentDocumentId the document to return children for, which must

* be a directory with MIME type of

* {@link Document#MIME_TYPE_DIR}.

* @see DocumentsProvider#queryChildDocuments(String, String[], String)

* @see #getDocumentId(Uri)

*/

public static Uri buildChildDocumentsUri(String authority, String parentDocumentId) {

return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)

.appendPath(PATH_DOCUMENT).appendPath(parentDocumentId).appendPath(PATH_CHILDREN)

.build();

}

/**

* Build URI representing the children of the target directory in a document

* provider. When queried, a provider will return zero or more rows with

* columns defined by {@link Document}.

*

* However, instead of directly accessing the target directory, the returned

* URI will leverage access granted through a subtree URI, typically

* returned by {@link Intent#ACTION_OPEN_DOCUMENT_TREE}. The target

* directory must be a descendant (child, grandchild, etc) of the subtree.

*

* This is typically used to access documents under a user-selected

* directory tree, since it doesn't require the user to separately confirm

* each new document access.

*

* @param treeUri the subtree to leverage to gain access to the target

* document. The target directory must be a descendant of this

* subtree.

* @param parentDocumentId the document to return children for, which the

* caller may not have direct access to, and which must be a

* directory with MIME type of {@link Document#MIME_TYPE_DIR}.

* @see Intent#ACTION_OPEN_DOCUMENT_TREE

* @see DocumentsProvider#isChildDocument(String, String)

* @see #buildChildDocumentsUri(String, String)

*/

public static Uri buildChildDocumentsUriUsingTree(Uri treeUri, String parentDocumentId) {

return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)

.authority(treeUri.getAuthority()).appendPath(PATH_TREE)

.appendPath(getTreeDocumentId(treeUri)).appendPath(PATH_DOCUMENT)

.appendPath(parentDocumentId).appendPath(PATH_CHILDREN).build();

}

/**

* Build URI representing a search for matching documents under a specific

* root in a document provider. When queried, a provider will return zero or

* more rows with columns defined by {@link Document}.

*

* @see DocumentsProvider#querySearchDocuments(String, String, String[])

* @see #getRootId(Uri)

* @see #getSearchDocumentsQuery(Uri)

*/

public static Uri buildSearchDocumentsUri(

String authority, String rootId, String query) {

return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)

.appendPath(PATH_ROOT).appendPath(rootId).appendPath(PATH_SEARCH)

.appendQueryParameter(PARAM_QUERY, query).build();

}

/**

* Check if the values match the query arguments.

*

* @param queryArgs the query arguments

* @param displayName the display time to check against

* @param mimeType the mime type to check against

* @param lastModified the last modified time to check against

* @param size the size to check against

* @hide

*/

public static boolean matchSearchQueryArguments(Bundle queryArgs, String displayName,

String mimeType, long lastModified, long size) {

if (queryArgs == null) {

return true;

}

final String argDisplayName = queryArgs.getString(QUERY_ARG_DISPLAY_NAME, "");

if (!argDisplayName.isEmpty()) {

// TODO (118795812) : Enhance the search string handled in DocumentsProvider

if (!displayName.toLowerCase().contains(argDisplayName.toLowerCase())) {

return false;

}

}

final long argFileSize = queryArgs.getLong(QUERY_ARG_FILE_SIZE_OVER, -1 /* defaultValue */);

if (argFileSize != -1 && size < argFileSize) {

return false;

}

final long argLastModified = queryArgs.getLong(QUERY_ARG_LAST_MODIFIED_AFTER,

-1 /* defaultValue */);

if (argLastModified != -1 && lastModified < argLastModified) {

return false;

}

final String[] argMimeTypes = queryArgs.getStringArray(QUERY_ARG_MIME_TYPES);

if (argMimeTypes != null && argMimeTypes.length > 0) {

mimeType = Intent.normalizeMimeType(mimeType);

for (String type : argMimeTypes) {

if (MimeTypeFilter.matches(mimeType, Intent.normalizeMimeType(type))) {

return true;

}

}

return false;

}

return true;

}

/**

* Get the handled query arguments from the query bundle. The handled arguments are

* {@link DocumentsContract#QUERY_ARG_EXCLUDE_MEDIA},

* {@link DocumentsContract#QUERY_ARG_DISPLAY_NAME},

* {@link DocumentsContract#QUERY_ARG_MIME_TYPES},

* {@link DocumentsContract#QUERY_ARG_FILE_SIZE_OVER} and

* {@link DocumentsContract#QUERY_ARG_LAST_MODIFIED_AFTER}.

*

* @param queryArgs the query arguments to be parsed.

* @return the handled query arguments

* @hide

*/

public static String[] getHandledQueryArguments(Bundle queryArgs) {

if (queryArgs == null) {

return new String[0];

}

final ArrayList args = new ArrayList<>();

if (queryArgs.keySet().contains(QUERY_ARG_EXCLUDE_MEDIA)) {

args.add(QUERY_ARG_EXCLUDE_MEDIA);

}

if (queryArgs.keySet().contains(QUERY_ARG_DISPLAY_NAME)) {

args.add(QUERY_ARG_DISPLAY_NAME);

}

if (queryArgs.keySet().contains(QUERY_ARG_FILE_SIZE_OVER)) {

args.add(QUERY_ARG_FILE_SIZE_OVER);

}

if (queryArgs.keySet().contains(QUERY_ARG_LAST_MODIFIED_AFTER)) {

args.add(QUERY_ARG_LAST_MODIFIED_AFTER);

}

if (queryArgs.keySet().contains(QUERY_ARG_MIME_TYPES)) {

args.add(QUERY_ARG_MIME_TYPES);

}

return args.toArray(new String[0]);

}

/**

* Test if the given URI represents a {@link Document} backed by a

* {@link DocumentsProvider}.

*

* @see #buildDocumentUri(String, String)

* @see #buildDocumentUriUsingTree(Uri, String)

*/

public static boolean isDocumentUri(Context context, @Nullable Uri uri) {

if (isContentUri(uri) && isDocumentsProvider(context, uri.getAuthority())) {

final List paths = uri.getPathSegments();

if (paths.size() == 2) {

return PATH_DOCUMENT.equals(paths.get(0));

} else if (paths.size() == 4) {

return PATH_TREE.equals(paths.get(0)) && PATH_DOCUMENT.equals(paths.get(2));

}

}

return false;

}

/**

* Test if the given URI represents all roots of the authority

* backed by {@link DocumentsProvider}.

*

* @see #buildRootsUri(String)

*/

public static boolean isRootsUri(@NonNull Context context, @Nullable Uri uri) {

Preconditions.checkNotNull(context, "context can not be null");

return isRootUri(context, uri, 1 /* pathSize */);

}

/**

* Test if the given URI represents specific root backed by {@link DocumentsProvider}.

*

* @see #buildRootUri(String, String)

*/

public static boolean isRootUri(@NonNull Context context, @Nullable Uri uri) {

Preconditions.checkNotNull(context, "context can not be null");

return isRootUri(context, uri, 2 /* pathSize */);

}

/** {@hide} */

public static boolean isContentUri(@Nullable Uri uri) {

return uri != null && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme());

}

/**

* Test if the given URI represents a {@link Document} tree.

*

* @see #buildTreeDocumentUri(String, String)

* @see #getTreeDocumentId(Uri)

*/

public static boolean isTreeUri(Uri uri) {

final List paths = uri.getPathSegments();

return (paths.size() >= 2 && PATH_TREE.equals(paths.get(0)));

}

private static boolean isRootUri(Context context, @Nullable Uri uri, int pathSize) {

if (isContentUri(uri) && isDocumentsProvider(context, uri.getAuthority())) {

final List paths = uri.getPathSegments();

return (paths.size() == pathSize && PATH_ROOT.equals(paths.get(0)));

}

return false;

}

private static boolean isDocumentsProvider(Context context, String authority) {

final Intent intent = new Intent(PROVIDER_INTERFACE);

final List infos = context.getPackageManager()

.queryIntentContentProviders(intent, 0);

for (ResolveInfo info : infos) {

if (authority.equals(info.providerInfo.authority)) {

return true;

}

}

return false;

}

/**

* Extract the {@link Root#COLUMN_ROOT_ID} from the given URI.

*/

public static String getRootId(Uri rootUri) {

final List paths = rootUri.getPathSegments();

if (paths.size() >= 2 && PATH_ROOT.equals(paths.get(0))) {

return paths.get(1);

}

throw new IllegalArgumentException("Invalid URI: " + rootUri);

}

/**

* Extract the {@link Document#COLUMN_DOCUMENT_ID} from the given URI.

*

* @see #isDocumentUri(Context, Uri)

*/

public static String getDocumentId(Uri documentUri) {

final List paths = documentUri.getPathSegments();

if (paths.size() >= 2 && PATH_DOCUMENT.equals(paths.get(0))) {

return paths.get(1);

}

if (paths.size() >= 4 && PATH_TREE.equals(paths.get(0))

&& PATH_DOCUMENT.equals(paths.get(2))) {

return paths.get(3);

}

throw new IllegalArgumentException("Invalid URI: " + documentUri);

}

/**

* Extract the via {@link Document#COLUMN_DOCUMENT_ID} from the given URI.

*/

public static String getTreeDocumentId(Uri documentUri) {

final List paths = documentUri.getPathSegments();

if (paths.size() >= 2 && PATH_TREE.equals(paths.get(0))) {

return paths.get(1);

}

throw new IllegalArgumentException("Invalid URI: " + documentUri);

}

/**

* Extract the search query from a URI built by

* {@link #buildSearchDocumentsUri(String, String, String)}.

*/

public static String getSearchDocumentsQuery(Uri searchDocumentsUri) {

return searchDocumentsUri.getQueryParameter(PARAM_QUERY);

}

/**

* Extract the search query from a Bundle

* {@link #QUERY_ARG_DISPLAY_NAME}.

* {@hide}

*/

public static String getSearchDocumentsQuery(@NonNull Bundle bundle) {

Preconditions.checkNotNull(bundle, "bundle can not be null");

return bundle.getString(QUERY_ARG_DISPLAY_NAME, "" /* defaultValue */);

}

/**

* Build URI that append the query parameter {@link PARAM_MANAGE} to

* enable the manage mode.

* @see DocumentsProvider#queryChildDocumentsForManage(String parentDocId, String[], String)

* {@hide}

*/

@SystemApi

public static @NonNull Uri setManageMode(@NonNull Uri uri) {

Preconditions.checkNotNull(uri, "uri can not be null");

return uri.buildUpon().appendQueryParameter(PARAM_MANAGE, "true").build();

}

/**

* Extract the manage mode from a URI built by

* {@link #setManageMode(Uri)}.

* {@hide}

*/

@SystemApi

public static boolean isManageMode(@NonNull Uri uri) {

Preconditions.checkNotNull(uri, "uri can not be null");

return uri.getBooleanQueryParameter(PARAM_MANAGE, false);

}

/**

* Return thumbnail representing the document at the given URI. Callers are

* responsible for their own in-memory caching.

*

* @param documentUri document to return thumbnail for, which must have

* {@link Document#FLAG_SUPPORTS_THUMBNAIL} set.

* @param size optimal thumbnail size desired. A provider may return a

* thumbnail of a different size, but never more than double the

* requested size.

* @param signal signal used to indicate if caller is no longer interested

* in the thumbnail.

* @return decoded thumbnail, or {@code null} if problem was encountered.

* @see DocumentsProvider#openDocumentThumbnail(String, Point,

* android.os.CancellationSignal)

*/

public static @Nullable Bitmap getDocumentThumbnail(@NonNull ContentResolver content,

@NonNull Uri documentUri, @NonNull Point size, @Nullable CancellationSignal signal)

throws FileNotFoundException {

try {

return ContentResolver.loadThumbnail(content, documentUri, Point.convert(size), signal,

ImageDecoder.ALLOCATOR_SOFTWARE);

} catch (Exception e) {

if (!(e instanceof OperationCanceledException)) {

Log.w(TAG, "Failed to load thumbnail for " + documentUri + ": " + e);

}

rethrowIfNecessary(e);

return null;

}

}

/**

* Create a new document with given MIME type and display name.

*

* @param parentDocumentUri directory with {@link Document#FLAG_DIR_SUPPORTS_CREATE}

* @param mimeType MIME type of new document

* @param displayName name of new document

* @return newly created document, or {@code null} if failed

*/

public static @Nullable Uri createDocument(@NonNull ContentResolver content,

@NonNull Uri parentDocumentUri, @NonNull String mimeType, @NonNull String displayName)

throws FileNotFoundException {

try {

final Bundle in = new Bundle();

in.putParcelable(DocumentsContract.EXTRA_URI, parentDocumentUri);

in.putString(Document.COLUMN_MIME_TYPE, mimeType);

in.putString(Document.COLUMN_DISPLAY_NAME, displayName);

final Bundle out = content.call(parentDocumentUri.getAuthority(),

METHOD_CREATE_DOCUMENT, null, in);

return out.getParcelable(DocumentsContract.EXTRA_URI);

} catch (Exception e) {

Log.w(TAG, "Failed to create document", e);

rethrowIfNecessary(e);

return null;

}

}

/**

* Test if a document is descendant (child, grandchild, etc) from the given

* parent.

*

* @param parentDocumentUri parent to verify against.

* @param childDocumentUri child to verify.

* @return if given document is a descendant of the given parent.

* @see Root#FLAG_SUPPORTS_IS_CHILD

*/

public static boolean isChildDocument(@NonNull ContentResolver content,

@NonNull Uri parentDocumentUri, @NonNull Uri childDocumentUri)

throws FileNotFoundException {

Preconditions.checkNotNull(content, "content can not be null");

Preconditions.checkNotNull(parentDocumentUri, "parentDocumentUri can not be null");

Preconditions.checkNotNull(childDocumentUri, "childDocumentUri can not be null");

try {

final Bundle in = new Bundle();

in.putParcelable(DocumentsContract.EXTRA_URI, parentDocumentUri);

in.putParcelable(DocumentsContract.EXTRA_TARGET_URI, childDocumentUri);

final Bundle out = content.call(parentDocumentUri.getAuthority(),

METHOD_IS_CHILD_DOCUMENT, null, in);

if (out == null) {

throw new RemoteException("Failed to get a response from isChildDocument query.");

}

if (!out.containsKey(DocumentsContract.EXTRA_RESULT)) {

throw new RemoteException("Response did not include result field..");

}

return out.getBoolean(DocumentsContract.EXTRA_RESULT);

} catch (Exception e) {

Log.w(TAG, "Failed to create document", e);

rethrowIfNecessary(e);

return false;

}

}

/**

* Change the display name of an existing document.

*

* If the underlying provider needs to create a new

* {@link Document#COLUMN_DOCUMENT_ID} to represent the updated display

* name, that new document is returned and the original document is no

* longer valid. Otherwise, the original document is returned.

*

* @param documentUri document with {@link Document#FLAG_SUPPORTS_RENAME}

* @param displayName updated name for document

* @return the existing or new document after the rename, or {@code null} if

* failed.

*/

public static @Nullable Uri renameDocument(@NonNull ContentResolver content,

@NonNull Uri documentUri, @NonNull String displayName) throws FileNotFoundException {

try {

final Bundle in = new Bundle();

in.putParcelable(DocumentsContract.EXTRA_URI, documentUri);

in.putString(Document.COLUMN_DISPLAY_NAME, displayName);

final Bundle out = content.call(documentUri.getAuthority(),

METHOD_RENAME_DOCUMENT, null, in);

final Uri outUri = out.getParcelable(DocumentsContract.EXTRA_URI);

return (outUri != null) ? outUri : documentUri;

} catch (Exception e) {

Log.w(TAG, "Failed to rename document", e);

rethrowIfNecessary(e);

return null;

}

}

/**

* Delete the given document.

*

* @param documentUri document with {@link Document#FLAG_SUPPORTS_DELETE}

* @return if the document was deleted successfully.

*/

public static boolean deleteDocument(@NonNull ContentResolver content, @NonNull Uri documentUri)

throws FileNotFoundException {

try {

final Bundle in = new Bundle();

in.putParcelable(DocumentsContract.EXTRA_URI, documentUri);

content.call(documentUri.getAuthority(),

METHOD_DELETE_DOCUMENT, null, in);

return true;

} catch (Exception e) {

Log.w(TAG, "Failed to delete document", e);

rethrowIfNecessary(e);

return false;

}

}

/**

* Copies the given document.

*

* @param sourceDocumentUri document with {@link Document#FLAG_SUPPORTS_COPY}

* @param targetParentDocumentUri document which will become a parent of the source

* document's copy.

* @return the copied document, or {@code null} if failed.

*/

public static @Nullable Uri copyDocument(@NonNull ContentResolver content,

@NonNull Uri sourceDocumentUri, @NonNull Uri targetParentDocumentUri)

throws FileNotFoundException {

try {

final Bundle in = new Bundle();

in.putParcelable(DocumentsContract.EXTRA_URI, sourceDocumentUri);

in.putParcelable(DocumentsContract.EXTRA_TARGET_URI, targetParentDocumentUri);

final Bundle out = content.call(sourceDocumentUri.getAuthority(),

METHOD_COPY_DOCUMENT, null, in);

return out.getParcelable(DocumentsContract.EXTRA_URI);

} catch (Exception e) {

Log.w(TAG, "Failed to copy document", e);

rethrowIfNecessary(e);

return null;

}

}

/**

* Moves the given document under a new parent.

*

* @param sourceDocumentUri document with {@link Document#FLAG_SUPPORTS_MOVE}

* @param sourceParentDocumentUri parent document of the document to move.

* @param targetParentDocumentUri document which will become a new parent of the source

* document.

* @return the moved document, or {@code null} if failed.

*/

public static @Nullable Uri moveDocument(@NonNull ContentResolver content,

@NonNull Uri sourceDocumentUri, @NonNull Uri sourceParentDocumentUri,

@NonNull Uri targetParentDocumentUri) throws FileNotFoundException {

try {

final Bundle in = new Bundle();

in.putParcelable(DocumentsContract.EXTRA_URI, sourceDocumentUri);

in.putParcelable(DocumentsContract.EXTRA_PARENT_URI, sourceParentDocumentUri);

in.putParcelable(DocumentsContract.EXTRA_TARGET_URI, targetParentDocumentUri);

final Bundle out = content.call(sourceDocumentUri.getAuthority(),

METHOD_MOVE_DOCUMENT, null, in);

return out.getParcelable(DocumentsContract.EXTRA_URI);

} catch (Exception e) {

Log.w(TAG, "Failed to move document", e);

rethrowIfNecessary(e);

return null;

}

}

/**

* Removes the given document from a parent directory.

*

*

In contrast to {@link #deleteDocument} it requires specifying the parent.

* This method is especially useful if the document can be in multiple parents.

*

* @param documentUri document with {@link Document#FLAG_SUPPORTS_REMOVE}

* @param parentDocumentUri parent document of the document to remove.

* @return true if the document was removed successfully.

*/

public static boolean removeDocument(@NonNull ContentResolver content, @NonNull Uri documentUri,

@NonNull Uri parentDocumentUri) throws FileNotFoundException {

try {

final Bundle in = new Bundle();

in.putParcelable(DocumentsContract.EXTRA_URI, documentUri);

in.putParcelable(DocumentsContract.EXTRA_PARENT_URI, parentDocumentUri);

content.call(documentUri.getAuthority(),

METHOD_REMOVE_DOCUMENT, null, in);

return true;

} catch (Exception e) {

Log.w(TAG, "Failed to remove document", e);

rethrowIfNecessary(e);

return false;

}

}

/**

* Ejects the given root. It throws {@link IllegalStateException} when ejection failed.

*

* @param rootUri root with {@link Root#FLAG_SUPPORTS_EJECT} to be ejected

*/

public static void ejectRoot(@NonNull ContentResolver content, @NonNull Uri rootUri) {

try {

final Bundle in = new Bundle();

in.putParcelable(DocumentsContract.EXTRA_URI, rootUri);

content.call(rootUri.getAuthority(),

METHOD_EJECT_ROOT, null, in);

} catch (Exception e) {

Log.w(TAG, "Failed to eject", e);

}

}

/**

* Returns metadata associated with the document. The type of metadata returned

* is specific to the document type. For example the data returned for an image

* file will likely consist primarily or solely of EXIF metadata.

*

*

The returned {@link Bundle} will contain zero or more entries depending

* on the type of data supported by the document provider.

*

*

*

A {@link DocumentsContract#METADATA_TYPES} containing a {@code String[]} value.

* The string array identifies the type or types of metadata returned. Each

* value in the can be used to access a {@link Bundle} of data

* containing that type of data.

*

An entry each for each type of returned metadata. Each set of metadata is

* itself represented as a bundle and accessible via a string key naming

* the type of data.

*

*

*

Example:

*

* Bundle metadata = DocumentsContract.getDocumentMetadata(client, imageDocUri, tags);

* if (metadata.containsKey(DocumentsContract.METADATA_EXIF)) {

* Bundle exif = metadata.getBundle(DocumentsContract.METADATA_EXIF);

* int imageLength = exif.getInt(ExifInterface.TAG_IMAGE_LENGTH);

* }

*

*

* @param documentUri a Document URI

* @return a Bundle of Bundles.

*/

public static @Nullable Bundle getDocumentMetadata(@NonNull ContentResolver content,

@NonNull Uri documentUri) throws FileNotFoundException {

Preconditions.checkNotNull(content, "content can not be null");

Preconditions.checkNotNull(documentUri, "documentUri can not be null");

try {

final Bundle in = new Bundle();

in.putParcelable(EXTRA_URI, documentUri);

return content.call(documentUri.getAuthority(),

METHOD_GET_DOCUMENT_METADATA, null, in);

} catch (Exception e) {

Log.w(TAG, "Failed to get document metadata");

rethrowIfNecessary(e);

return null;

}

}

/**

* Finds the canonical path from the top of the document tree.

*

* The {@link Path#getPath()} of the return value contains the document ID

* of all documents along the path from the top the document tree to the

* requested document, both inclusive.

*

* The {@link Path#getRootId()} of the return value returns {@code null}.

*

* @param treeUri treeUri of the document which path is requested.

* @return the path of the document, or {@code null} if failed.

* @see DocumentsProvider#findDocumentPath(String, String)

*/

public static @Nullable Path findDocumentPath(@NonNull ContentResolver content,

@NonNull Uri treeUri) throws FileNotFoundException {

try {

final Bundle in = new Bundle();

in.putParcelable(DocumentsContract.EXTRA_URI, treeUri);

final Bundle out = content.call(treeUri.getAuthority(),

METHOD_FIND_DOCUMENT_PATH, null, in);

return out.getParcelable(DocumentsContract.EXTRA_RESULT);

} catch (Exception e) {

Log.w(TAG, "Failed to find path", e);

rethrowIfNecessary(e);

return null;

}

}

/**

* Creates an intent for obtaining a web link for the specified document.

*

*

Note, that due to internal limitations, if there is already a web link

* intent created for the specified document but with different options,

* then it may be overridden.

*

*

Providers are required to show confirmation UI for all new permissions granted

* for the linked document.

*

*

If list of recipients is known, then it should be passed in options as

* {@link Intent#EXTRA_EMAIL} as a list of email addresses. Note, that

* this is just a hint for the provider, which can ignore the list. In either

* case the provider is required to show a UI for letting the user confirm

* any new permission grants.

*

*

Note, that the entire options bundle will be sent to the provider

* backing the passed uri. Make sure that you trust the provider

* before passing any sensitive information.

*

*

Since this API may show a UI, it cannot be called from background.

*

*

In order to obtain the Web Link use code like this:

*

* void onSomethingHappened() {

* IntentSender sender = DocumentsContract.createWebLinkIntent(...);

* if (sender != null) {

* startIntentSenderForResult(

* sender,

* WEB_LINK_REQUEST_CODE,

* null, 0, 0, 0, null);

* }

* }

*

* (...)

*

* void onActivityResult(int requestCode, int resultCode, Intent data) {

* if (requestCode == WEB_LINK_REQUEST_CODE && resultCode == RESULT_OK) {

* Uri weblinkUri = data.getData();

* ...

* }

* }

*

*

* @param uri uri for the document to create a link to.

* @param options Extra information for generating the link.

* @return an intent sender to obtain the web link, or null if the document

* is not linkable, or creating the intent sender failed.

* @see DocumentsProvider#createWebLinkIntent(String, Bundle)

* @see Intent#EXTRA_EMAIL

*/

public static @Nullable IntentSender createWebLinkIntent(@NonNull ContentResolver content,

@NonNull Uri uri, @Nullable Bundle options) throws FileNotFoundException {

try {

final Bundle in = new Bundle();

in.putParcelable(DocumentsContract.EXTRA_URI, uri);

// Options may be provider specific, so put them in a separate bundle to

// avoid overriding the Uri.

if (options != null) {

in.putBundle(EXTRA_OPTIONS, options);

}

final Bundle out = content.call(uri.getAuthority(),

METHOD_CREATE_WEB_LINK_INTENT, null, in);

return out.getParcelable(DocumentsContract.EXTRA_RESULT);

} catch (Exception e) {

Log.w(TAG, "Failed to create a web link intent", e);

rethrowIfNecessary(e);

return null;

}

}

/**

* Open the given image for thumbnail purposes, using any embedded EXIF

* thumbnail if available, and providing orientation hints from the parent

* image.

*

* @hide

*/

public static AssetFileDescriptor openImageThumbnail(File file) throws FileNotFoundException {

final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(

file, ParcelFileDescriptor.MODE_READ_ONLY);

try {

final ExifInterface exif = new ExifInterface(file.getAbsolutePath());

final long[] thumb = exif.getThumbnailRange();

if (thumb != null) {

// If we use thumb to decode, we need to handle the rotation by ourselves.

Bundle extras = null;

switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1)) {

case ExifInterface.ORIENTATION_ROTATE_90:

extras = new Bundle(1);

extras.putInt(EXTRA_ORIENTATION, 90);

break;

case ExifInterface.ORIENTATION_ROTATE_180:

extras = new Bundle(1);

extras.putInt(EXTRA_ORIENTATION, 180);

break;

case ExifInterface.ORIENTATION_ROTATE_270:

extras = new Bundle(1);

extras.putInt(EXTRA_ORIENTATION, 270);

break;

}

return new AssetFileDescriptor(pfd, thumb[0], thumb[1], extras);

}

} catch (IOException e) {

}

// Do full file decoding, we don't need to handle the orientation

return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH, null);

}

private static void rethrowIfNecessary(Exception e) throws FileNotFoundException {

// We only want to throw applications targetting O and above

if (VMRuntime.getRuntime().getTargetSdkVersion() >= Build.VERSION_CODES.O) {

if (e instanceof ParcelableException) {

((ParcelableException) e).maybeRethrow(FileNotFoundException.class);

} else if (e instanceof RemoteException) {

((RemoteException) e).rethrowAsRuntimeException();

} else if (e instanceof RuntimeException) {

throw (RuntimeException) e;

}

}

}

/**

* Holds a path from a document to a particular document under it. It

* may also contains the root ID where the path resides.

*/

public static final class Path implements Parcelable {

private final @Nullable String mRootId;

private final List mPath;

/**

* Creates a Path.

*

* @param rootId the ID of the root. May be null.

* @param path the list of document ID from the parent document at

* position 0 to the child document.

*/

public Path(@Nullable String rootId, List path) {

checkCollectionNotEmpty(path, "path");

checkCollectionElementsNotNull(path, "path");

mRootId = rootId;

mPath = path;

}

/**

* Returns the root id or null if the calling package doesn't have

* permission to access root information.

*/

public @Nullable String getRootId() {

return mRootId;

}

/**

* Returns the path. The path is trimmed to the top of tree if

* calling package doesn't have permission to access those

* documents.

*/

public List getPath() {

return mPath;

}

@Override

public boolean equals(Object o) {

if (this == o) {

return true;

}

if (o == null || !(o instanceof Path)) {

return false;

}

Path path = (Path) o;

return Objects.equals(mRootId, path.mRootId) &&

Objects.equals(mPath, path.mPath);

}

@Override

public int hashCode() {

return Objects.hash(mRootId, mPath);

}

@Override

public String toString() {

return new StringBuilder()

.append("DocumentsContract.Path{")

.append("rootId=")

.append(mRootId)

.append(", path=")

.append(mPath)

.append("}")

.toString();

}

@Override

public void writeToParcel(Parcel dest, int flags) {

dest.writeString(mRootId);

dest.writeStringList(mPath);

}

@Override

public int describeContents() {

return 0;

}

public static final @android.annotation.NonNull Creator CREATOR = new Creator() {

@Override

public Path createFromParcel(Parcel in) {

final String rootId = in.readString();

final List path = in.createStringArrayList();

return new Path(rootId, path);

}

@Override

public Path[] newArray(int size) {

return new Path[size];

}

};

}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值