Loading core/java/android/content/MimeTypeFilter.java 0 → 100644 +154 −0 Original line number Diff line number Diff line /* * 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. * * <p>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. * * <p><em>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.</em> * * <p>MIME types can be null or ill-formatted. In such case they won't match anything. * * <p>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<String> list = new ArrayList<>(); final String[] filterParts = filter.split("/"); for (String mimeType : mimeTypes) { final String[] mimeTypeParts = mimeType.split("/"); if (mimeTypeAgainstFilter(mimeTypeParts, filterParts)) { list.add(mimeType); } } return list.toArray(new String[list.size()]); } } core/java/android/provider/DocumentsContract.java +143 −12 Original line number Diff line number Diff line Loading @@ -16,12 +16,11 @@ package android.provider; import static android.system.OsConstants.SEEK_SET; import static com.android.internal.util.Preconditions.checkArgument; 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.UnsupportedAppUsage; import android.content.ContentProviderClient; Loading @@ -29,13 +28,12 @@ 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.BitmapFactory; import android.graphics.ImageDecoder; import android.graphics.Matrix; import android.graphics.Point; import android.media.ExifInterface; import android.net.Uri; Loading @@ -50,20 +48,13 @@ import android.os.Parcelable; import android.os.ParcelableException; import android.os.RemoteException; import android.os.storage.StorageVolume; import android.system.ErrnoException; import android.system.Os; import android.util.DataUnit; import android.util.Log; import android.util.Size; import libcore.io.IoUtils; import java.io.BufferedInputStream; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Objects; Loading Loading @@ -112,6 +103,54 @@ public final class DocumentsContract { /** {@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) * {@hide} */ 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) * {@hide} */ 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) * {@hide} */ 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 * {@hide} */ public static final String QUERY_ARG_LAST_MODIFIED_AFTER = "android:query-arg-last-modified-after"; /** * Sets the desired initial location visible to user when file chooser is shown. * Loading Loading @@ -928,6 +967,89 @@ public final class DocumentsContract { .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_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<String> args = new ArrayList<>(); 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}. Loading Loading @@ -1052,6 +1174,15 @@ public final class DocumentsContract { 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) { return bundle.getString(QUERY_ARG_DISPLAY_NAME, "" /* defaultValue */); } /** {@hide} */ @UnsupportedAppUsage public static Uri setManageMode(Uri uri) { Loading core/java/android/provider/DocumentsProvider.java +54 −23 Original line number Diff line number Diff line Loading @@ -32,7 +32,6 @@ import static android.provider.DocumentsContract.buildDocumentUriMaybeUsingTree; import static android.provider.DocumentsContract.buildTreeDocumentUri; import static android.provider.DocumentsContract.getDocumentId; import static android.provider.DocumentsContract.getRootId; import static android.provider.DocumentsContract.getSearchDocumentsQuery; import static android.provider.DocumentsContract.getTreeDocumentId; import static android.provider.DocumentsContract.isTreeUri; Loading @@ -47,6 +46,7 @@ import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.IntentSender; import android.content.MimeTypeFilter; import android.content.UriMatcher; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; Loading Loading @@ -650,6 +650,55 @@ public abstract class DocumentsProvider extends ContentProvider { throw new UnsupportedOperationException("Search not supported"); } /** * Return documents that match the given query under the requested * root. The returned documents should be sorted by relevance in descending * order. How documents are matched against the query string is an * implementation detail left to each provider, but it's suggested that at * least {@link Document#COLUMN_DISPLAY_NAME} be matched in a * case-insensitive fashion. * <p> * 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. * <p> * 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. * Loading Loading @@ -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, Loading @@ -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( Loading @@ -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); Loading Loading @@ -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 }; } } Loading Loading @@ -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; } } } core/java/com/android/internal/content/FileSystemProvider.java +45 −6 Original line number Diff line number Diff line Loading @@ -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<String> exclusion) File folder, String[] projection, Set<String> exclusion, Bundle queryArgs) throws FileNotFoundException { query = query.toLowerCase(); final MatrixCursor result = new MatrixCursor(resolveProjection(projection)); final LinkedList<File> pending = new LinkedList<>(); pending.add(folder); Loading @@ -407,11 +411,18 @@ public abstract class FileSystemProvider extends DocumentsProvider { pending.add(child); } } if (file.getName().toLowerCase().contains(query) && !exclusion.contains(file.getAbsolutePath())) { if (!exclusion.contains(file.getAbsolutePath()) && matchSearchQueryArguments(file, queryArgs)) { includeFile(result, null, file); } } final String[] handledQueryArgs = DocumentsContract.getHandledQueryArguments(queryArgs); if (handledQueryArgs.length > 0) { final Bundle extras = new Bundle(); extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, handledQueryArgs); result.setExtras(extras); } return result; } Loading Loading @@ -457,6 +468,34 @@ public abstract class FileSystemProvider extends DocumentsProvider { } } /** * Test if the file matches the query arguments. * * @param file the file to test * @param queryArgs the query arguments */ private boolean matchSearchQueryArguments(File file, Bundle queryArgs) { if (file == null) { return false; } final String fileMimeType; final String fileName = file.getName(); if (file.isDirectory()) { fileMimeType = DocumentsContract.Document.MIME_TYPE_DIR; } else { int dotPos = fileName.lastIndexOf('.'); if (dotPos < 0) { return false; } final String extension = fileName.substring(dotPos + 1); fileMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); } return DocumentsContract.matchSearchQueryArguments(queryArgs, fileName, fileMimeType, file.lastModified(), file.length()); } private void scanFile(File visibleFile) { final Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); intent.setData(Uri.fromFile(visibleFile)); Loading packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java +2 −2 Original line number Diff line number Diff line Loading @@ -541,14 +541,14 @@ public class ExternalStorageProvider extends FileSystemProvider { } @Override public Cursor querySearchDocuments(String rootId, String query, String[] projection) public Cursor querySearchDocuments(String rootId, String[] projection, Bundle queryArgs) throws FileNotFoundException { final File parent; synchronized (mRootsLock) { parent = mRoots.get(rootId).path; } return querySearchDocuments(parent, query, projection, Collections.emptySet()); return querySearchDocuments(parent, projection, Collections.emptySet(), queryArgs); } @Override Loading Loading
core/java/android/content/MimeTypeFilter.java 0 → 100644 +154 −0 Original line number Diff line number Diff line /* * 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. * * <p>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. * * <p><em>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.</em> * * <p>MIME types can be null or ill-formatted. In such case they won't match anything. * * <p>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<String> list = new ArrayList<>(); final String[] filterParts = filter.split("/"); for (String mimeType : mimeTypes) { final String[] mimeTypeParts = mimeType.split("/"); if (mimeTypeAgainstFilter(mimeTypeParts, filterParts)) { list.add(mimeType); } } return list.toArray(new String[list.size()]); } }
core/java/android/provider/DocumentsContract.java +143 −12 Original line number Diff line number Diff line Loading @@ -16,12 +16,11 @@ package android.provider; import static android.system.OsConstants.SEEK_SET; import static com.android.internal.util.Preconditions.checkArgument; 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.UnsupportedAppUsage; import android.content.ContentProviderClient; Loading @@ -29,13 +28,12 @@ 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.BitmapFactory; import android.graphics.ImageDecoder; import android.graphics.Matrix; import android.graphics.Point; import android.media.ExifInterface; import android.net.Uri; Loading @@ -50,20 +48,13 @@ import android.os.Parcelable; import android.os.ParcelableException; import android.os.RemoteException; import android.os.storage.StorageVolume; import android.system.ErrnoException; import android.system.Os; import android.util.DataUnit; import android.util.Log; import android.util.Size; import libcore.io.IoUtils; import java.io.BufferedInputStream; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Objects; Loading Loading @@ -112,6 +103,54 @@ public final class DocumentsContract { /** {@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) * {@hide} */ 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) * {@hide} */ 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) * {@hide} */ 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 * {@hide} */ public static final String QUERY_ARG_LAST_MODIFIED_AFTER = "android:query-arg-last-modified-after"; /** * Sets the desired initial location visible to user when file chooser is shown. * Loading Loading @@ -928,6 +967,89 @@ public final class DocumentsContract { .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_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<String> args = new ArrayList<>(); 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}. Loading Loading @@ -1052,6 +1174,15 @@ public final class DocumentsContract { 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) { return bundle.getString(QUERY_ARG_DISPLAY_NAME, "" /* defaultValue */); } /** {@hide} */ @UnsupportedAppUsage public static Uri setManageMode(Uri uri) { Loading
core/java/android/provider/DocumentsProvider.java +54 −23 Original line number Diff line number Diff line Loading @@ -32,7 +32,6 @@ import static android.provider.DocumentsContract.buildDocumentUriMaybeUsingTree; import static android.provider.DocumentsContract.buildTreeDocumentUri; import static android.provider.DocumentsContract.getDocumentId; import static android.provider.DocumentsContract.getRootId; import static android.provider.DocumentsContract.getSearchDocumentsQuery; import static android.provider.DocumentsContract.getTreeDocumentId; import static android.provider.DocumentsContract.isTreeUri; Loading @@ -47,6 +46,7 @@ import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.IntentSender; import android.content.MimeTypeFilter; import android.content.UriMatcher; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; Loading Loading @@ -650,6 +650,55 @@ public abstract class DocumentsProvider extends ContentProvider { throw new UnsupportedOperationException("Search not supported"); } /** * Return documents that match the given query under the requested * root. The returned documents should be sorted by relevance in descending * order. How documents are matched against the query string is an * implementation detail left to each provider, but it's suggested that at * least {@link Document#COLUMN_DISPLAY_NAME} be matched in a * case-insensitive fashion. * <p> * 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. * <p> * 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. * Loading Loading @@ -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, Loading @@ -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( Loading @@ -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); Loading Loading @@ -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 }; } } Loading Loading @@ -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; } } }
core/java/com/android/internal/content/FileSystemProvider.java +45 −6 Original line number Diff line number Diff line Loading @@ -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<String> exclusion) File folder, String[] projection, Set<String> exclusion, Bundle queryArgs) throws FileNotFoundException { query = query.toLowerCase(); final MatrixCursor result = new MatrixCursor(resolveProjection(projection)); final LinkedList<File> pending = new LinkedList<>(); pending.add(folder); Loading @@ -407,11 +411,18 @@ public abstract class FileSystemProvider extends DocumentsProvider { pending.add(child); } } if (file.getName().toLowerCase().contains(query) && !exclusion.contains(file.getAbsolutePath())) { if (!exclusion.contains(file.getAbsolutePath()) && matchSearchQueryArguments(file, queryArgs)) { includeFile(result, null, file); } } final String[] handledQueryArgs = DocumentsContract.getHandledQueryArguments(queryArgs); if (handledQueryArgs.length > 0) { final Bundle extras = new Bundle(); extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, handledQueryArgs); result.setExtras(extras); } return result; } Loading Loading @@ -457,6 +468,34 @@ public abstract class FileSystemProvider extends DocumentsProvider { } } /** * Test if the file matches the query arguments. * * @param file the file to test * @param queryArgs the query arguments */ private boolean matchSearchQueryArguments(File file, Bundle queryArgs) { if (file == null) { return false; } final String fileMimeType; final String fileName = file.getName(); if (file.isDirectory()) { fileMimeType = DocumentsContract.Document.MIME_TYPE_DIR; } else { int dotPos = fileName.lastIndexOf('.'); if (dotPos < 0) { return false; } final String extension = fileName.substring(dotPos + 1); fileMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); } return DocumentsContract.matchSearchQueryArguments(queryArgs, fileName, fileMimeType, file.lastModified(), file.length()); } private void scanFile(File visibleFile) { final Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); intent.setData(Uri.fromFile(visibleFile)); Loading
packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java +2 −2 Original line number Diff line number Diff line Loading @@ -541,14 +541,14 @@ public class ExternalStorageProvider extends FileSystemProvider { } @Override public Cursor querySearchDocuments(String rootId, String query, String[] projection) public Cursor querySearchDocuments(String rootId, String[] projection, Bundle queryArgs) throws FileNotFoundException { final File parent; synchronized (mRootsLock) { parent = mRoots.get(rootId).path; } return querySearchDocuments(parent, query, projection, Collections.emptySet()); return querySearchDocuments(parent, projection, Collections.emptySet(), queryArgs); } @Override Loading