diff --git a/core/java/android/content/MimeTypeFilter.java b/core/java/android/content/MimeTypeFilter.java new file mode 100644 index 0000000000000..1c26fd917f765 --- /dev/null +++ b/core/java/android/content/MimeTypeFilter.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2018 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.content; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import java.util.ArrayList; + +/** + * Provides utility methods for matching MIME type filters used in ContentProvider. + * + *
Wildcards are allowed only instead of the entire type or subtype with a tree prefix. + * Eg. image\/*, *\/* is a valid filter and will match image/jpeg, but image/j* is invalid and + * it will not match image/jpeg. Suffixes and parameters are not supported, and they are treated + * as part of the subtype during matching. Neither type nor subtype can be empty. + * + *
Note: MIME type matching in the Android framework is case-sensitive, unlike the formal + * RFC definitions. As a result, you should always write these elements with lower case letters, + * or use {@link android.content.Intent#normalizeMimeType} to ensure that they are converted to + * lower case. + * + *
MIME types can be null or ill-formatted. In such case they won't match anything. + * + *
MIME type filters must be correctly formatted, or an exception will be thrown.
+ * Copied from support library.
+ * {@hide}
+ */
+public final class MimeTypeFilter {
+
+ private MimeTypeFilter() {
+ }
+
+ private static boolean mimeTypeAgainstFilter(
+ @NonNull String[] mimeTypeParts, @NonNull String[] filterParts) {
+ if (filterParts.length != 2) {
+ throw new IllegalArgumentException(
+ "Ill-formatted MIME type filter. Must be type/subtype.");
+ }
+ if (filterParts[0].isEmpty() || filterParts[1].isEmpty()) {
+ throw new IllegalArgumentException(
+ "Ill-formatted MIME type filter. Type or subtype empty.");
+ }
+ if (mimeTypeParts.length != 2) {
+ return false;
+ }
+ if (!"*".equals(filterParts[0])
+ && !filterParts[0].equals(mimeTypeParts[0])) {
+ return false;
+ }
+ if (!"*".equals(filterParts[1])
+ && !filterParts[1].equals(mimeTypeParts[1])) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Matches one nullable MIME type against one MIME type filter.
+ * @return True if the {@code mimeType} matches the {@code filter}.
+ */
+ public static boolean matches(@Nullable String mimeType, @NonNull String filter) {
+ if (mimeType == null) {
+ return false;
+ }
+
+ final String[] mimeTypeParts = mimeType.split("/");
+ final String[] filterParts = filter.split("/");
+
+ return mimeTypeAgainstFilter(mimeTypeParts, filterParts);
+ }
+
+ /**
+ * Matches one nullable MIME type against an array of MIME type filters.
+ * @return The first matching filter, or null if nothing matches.
+ */
+ @Nullable
+ public static String matches(
+ @Nullable String mimeType, @NonNull String[] filters) {
+ if (mimeType == null) {
+ return null;
+ }
+
+ final String[] mimeTypeParts = mimeType.split("/");
+ for (String filter : filters) {
+ final String[] filterParts = filter.split("/");
+ if (mimeTypeAgainstFilter(mimeTypeParts, filterParts)) {
+ return filter;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Matches multiple MIME types against an array of MIME type filters.
+ * @return The first matching MIME type, or null if nothing matches.
+ */
+ @Nullable
+ public static String matches(
+ @Nullable String[] mimeTypes, @NonNull String filter) {
+ if (mimeTypes == null) {
+ return null;
+ }
+
+ final String[] filterParts = filter.split("/");
+ for (String mimeType : mimeTypes) {
+ final String[] mimeTypeParts = mimeType.split("/");
+ if (mimeTypeAgainstFilter(mimeTypeParts, filterParts)) {
+ return mimeType;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Matches multiple MIME types against an array of MIME type filters.
+ * @return The list of matching MIME types, or empty array if nothing matches.
+ */
+ @NonNull
+ public static String[] matchesMany(
+ @Nullable String[] mimeTypes, @NonNull String filter) {
+ if (mimeTypes == null) {
+ return new String[] {};
+ }
+
+ final ArrayList
+ * If your provider is cloud-based, and you have some data cached or pinned
+ * locally, you may return the local data immediately, setting
+ * {@link DocumentsContract#EXTRA_LOADING} on the Cursor to indicate that
+ * you are still fetching additional data. Then, when the network data is
+ * available, you can send a change notification to trigger a requery and
+ * return the complete contents.
+ *
+ * To support change notifications, you must
+ * {@link Cursor#setNotificationUri(ContentResolver, Uri)} with a relevant
+ * Uri, such as {@link DocumentsContract#buildSearchDocumentsUri(String,
+ * String, String)}. Then you can call {@link ContentResolver#notifyChange(Uri,
+ * android.database.ContentObserver, boolean)} with that Uri to send change
+ * notifications.
+ *
+ * @param rootId the root to search under.
+ * @param projection list of {@link Document} columns to put into the
+ * cursor. If {@code null} all supported columns should be
+ * included.
+ * @param queryArgs the query arguments.
+ * {@link DocumentsContract#QUERY_ARG_DISPLAY_NAME},
+ * {@link DocumentsContract#QUERY_ARG_MIME_TYPES},
+ * {@link DocumentsContract#QUERY_ARG_FILE_SIZE_OVER},
+ * {@link DocumentsContract#QUERY_ARG_LAST_MODIFIED_AFTER}.
+ * @return cursor containing search result. Include
+ * {@link ContentResolver#EXTRA_HONORED_ARGS} in {@link Cursor}
+ * extras {@link Bundle} when any QUERY_ARG_* value was honored
+ * during the preparation of the results.
+ *
+ * @see ContentResolver#EXTRA_HONORED_ARGS
+ * @see DocumentsContract#EXTRA_LOADING
+ * @see DocumentsContract#EXTRA_INFO
+ * @see DocumentsContract#EXTRA_ERROR
+ * {@hide}
+ */
+ @SuppressWarnings("unused")
+ public Cursor querySearchDocuments(String rootId, String[] projection, Bundle queryArgs)
+ throws FileNotFoundException {
+ return querySearchDocuments(rootId, DocumentsContract.getSearchDocumentsQuery(queryArgs),
+ projection);
+ }
+
/**
* Ejects the root. Throws {@link IllegalStateException} if ejection failed.
*
@@ -795,7 +844,7 @@ public abstract class DocumentsProvider extends ContentProvider {
* {@link #queryDocument(String, String[])},
* {@link #queryRecentDocuments(String, String[])},
* {@link #queryRoots(String[])}, and
- * {@link #querySearchDocuments(String, String, String[])}.
+ * {@link #querySearchDocuments(String, String[], Bundle)}.
*/
@Override
public Cursor query(Uri uri, String[] projection, String selection,
@@ -812,7 +861,7 @@ public abstract class DocumentsProvider extends ContentProvider {
* @see #queryRecentDocuments(String, String[], Bundle, CancellationSignal)
* @see #queryDocument(String, String[])
* @see #queryChildDocuments(String, String[], String)
- * @see #querySearchDocuments(String, String, String[])
+ * @see #querySearchDocuments(String, String[], Bundle)
*/
@Override
public final Cursor query(
@@ -825,8 +874,7 @@ public abstract class DocumentsProvider extends ContentProvider {
return queryRecentDocuments(
getRootId(uri), projection, queryArgs, cancellationSignal);
case MATCH_SEARCH:
- return querySearchDocuments(
- getRootId(uri), getSearchDocumentsQuery(uri), projection);
+ return querySearchDocuments(getRootId(uri), projection, queryArgs);
case MATCH_DOCUMENT:
case MATCH_DOCUMENT_TREE:
enforceTree(uri);
@@ -1301,7 +1349,7 @@ public abstract class DocumentsProvider extends ContentProvider {
final long flags =
cursor.getLong(cursor.getColumnIndexOrThrow(Document.COLUMN_FLAGS));
if ((flags & Document.FLAG_VIRTUAL_DOCUMENT) == 0 && mimeType != null &&
- mimeTypeMatches(mimeTypeFilter, mimeType)) {
+ MimeTypeFilter.matches(mimeType, mimeTypeFilter)) {
return new String[] { mimeType };
}
}
@@ -1354,21 +1402,4 @@ public abstract class DocumentsProvider extends ContentProvider {
// For any other yet unhandled case, let the provider subclass handle it.
return openTypedDocument(documentId, mimeTypeFilter, opts, signal);
}
-
- /**
- * @hide
- */
- public static boolean mimeTypeMatches(String filter, String test) {
- if (test == null) {
- return false;
- } else if (filter == null || "*/*".equals(filter)) {
- return true;
- } else if (filter.equals(test)) {
- return true;
- } else if (filter.endsWith("/*")) {
- return filter.regionMatches(0, test, 0, filter.indexOf('/'));
- } else {
- return false;
- }
- }
}
diff --git a/core/java/com/android/internal/content/FileSystemProvider.java b/core/java/com/android/internal/content/FileSystemProvider.java
index 81dab2f6aeef0..8bc90a8913525 100644
--- a/core/java/com/android/internal/content/FileSystemProvider.java
+++ b/core/java/com/android/internal/content/FileSystemProvider.java
@@ -389,14 +389,18 @@ public abstract class FileSystemProvider extends DocumentsProvider {
* @param query the search condition used to match file names
* @param projection projection of the returned cursor
* @param exclusion absolute file paths to exclude from result
- * @return cursor containing search result
+ * @param queryArgs the query arguments for search
+ * @return cursor containing search result. Include
+ * {@link ContentResolver#EXTRA_HONORED_ARGS} in {@link Cursor}
+ * extras {@link Bundle} when any QUERY_ARG_* value was honored
+ * during the preparation of the results.
* @throws FileNotFoundException when root folder doesn't exist or search fails
+ *
+ * @see ContentResolver#EXTRA_HONORED_ARGS
*/
protected final Cursor querySearchDocuments(
- File folder, String query, String[] projection, Set