diff options
-rw-r--r-- | api/current.txt | 14 | ||||
-rw-r--r-- | api/system-current.txt | 14 | ||||
-rw-r--r-- | api/test-current.txt | 14 | ||||
-rw-r--r-- | core/java/android/content/ContentProvider.java | 34 | ||||
-rw-r--r-- | core/java/android/content/ContentResolver.java | 150 | ||||
-rw-r--r-- | core/java/android/provider/DocumentsProvider.java | 87 | ||||
-rw-r--r-- | packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java | 2 | ||||
-rw-r--r-- | packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java | 22 |
8 files changed, 294 insertions, 43 deletions
diff --git a/api/current.txt b/api/current.txt index d8c242bb9ad8..54eb234ce9ac 100644 --- a/api/current.txt +++ b/api/current.txt @@ -8202,9 +8202,14 @@ package android.content { field public static final java.lang.String EXTRA_SIZE = "android.content.extra.SIZE"; field public static final int NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS = 2; // 0x2 field public static final int NOTIFY_SYNC_TO_NETWORK = 1; // 0x1 - field public static final java.lang.String QUERY_ARG_SELECTION = "android:query-selection"; - field public static final java.lang.String QUERY_ARG_SELECTION_ARGS = "android:query-selection-args"; - field public static final java.lang.String QUERY_ARG_SORT_ORDER = "android:query-sort-order"; + field public static final java.lang.String QUERY_ARG_SORT_COLLATION = "android:query-sort-collation"; + field public static final java.lang.String QUERY_ARG_SORT_COLUMNS = "android:query-sort-columns"; + field public static final java.lang.String QUERY_ARG_SORT_DIRECTION = "android:query-sort-direction"; + field public static final java.lang.String QUERY_ARG_SQL_SELECTION = "android:query-sql-selection"; + field public static final java.lang.String QUERY_ARG_SQL_SELECTION_ARGS = "android:query-sql-selection-args"; + field public static final java.lang.String QUERY_ARG_SQL_SORT_ORDER = "android:query-sql-sort-order"; + field public static final int QUERY_SORT_DIRECTION_ASCENDING = 0; // 0x0 + field public static final int QUERY_SORT_DIRECTION_DESCENDING = 1; // 0x1 field public static final java.lang.String SCHEME_ANDROID_RESOURCE = "android.resource"; field public static final java.lang.String SCHEME_CONTENT = "content"; field public static final java.lang.String SCHEME_FILE = "file"; @@ -32689,7 +32694,10 @@ package android.provider { method public final android.content.res.AssetFileDescriptor openTypedAssetFile(android.net.Uri, java.lang.String, android.os.Bundle, android.os.CancellationSignal) throws java.io.FileNotFoundException; method public android.content.res.AssetFileDescriptor openTypedDocument(java.lang.String, java.lang.String, android.os.Bundle, android.os.CancellationSignal) throws java.io.FileNotFoundException; method public final android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String); + method public final android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, android.os.CancellationSignal); + method public final android.database.Cursor query(android.net.Uri, java.lang.String[], android.os.Bundle, android.os.CancellationSignal); method public abstract android.database.Cursor queryChildDocuments(java.lang.String, java.lang.String[], java.lang.String) throws java.io.FileNotFoundException; + method public android.database.Cursor queryChildDocuments(java.lang.String, java.lang.String[], android.os.Bundle) throws java.io.FileNotFoundException; method public abstract android.database.Cursor queryDocument(java.lang.String, java.lang.String[]) throws java.io.FileNotFoundException; method public android.database.Cursor queryRecentDocuments(java.lang.String, java.lang.String[]) throws java.io.FileNotFoundException; method public abstract android.database.Cursor queryRoots(java.lang.String[]) throws java.io.FileNotFoundException; diff --git a/api/system-current.txt b/api/system-current.txt index 0cd2452e1a7c..6dce43f6fbee 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -8552,9 +8552,14 @@ package android.content { field public static final java.lang.String EXTRA_SIZE = "android.content.extra.SIZE"; field public static final int NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS = 2; // 0x2 field public static final int NOTIFY_SYNC_TO_NETWORK = 1; // 0x1 - field public static final java.lang.String QUERY_ARG_SELECTION = "android:query-selection"; - field public static final java.lang.String QUERY_ARG_SELECTION_ARGS = "android:query-selection-args"; - field public static final java.lang.String QUERY_ARG_SORT_ORDER = "android:query-sort-order"; + field public static final java.lang.String QUERY_ARG_SORT_COLLATION = "android:query-sort-collation"; + field public static final java.lang.String QUERY_ARG_SORT_COLUMNS = "android:query-sort-columns"; + field public static final java.lang.String QUERY_ARG_SORT_DIRECTION = "android:query-sort-direction"; + field public static final java.lang.String QUERY_ARG_SQL_SELECTION = "android:query-sql-selection"; + field public static final java.lang.String QUERY_ARG_SQL_SELECTION_ARGS = "android:query-sql-selection-args"; + field public static final java.lang.String QUERY_ARG_SQL_SORT_ORDER = "android:query-sql-sort-order"; + field public static final int QUERY_SORT_DIRECTION_ASCENDING = 0; // 0x0 + field public static final int QUERY_SORT_DIRECTION_DESCENDING = 1; // 0x1 field public static final java.lang.String SCHEME_ANDROID_RESOURCE = "android.resource"; field public static final java.lang.String SCHEME_CONTENT = "content"; field public static final java.lang.String SCHEME_FILE = "file"; @@ -35445,7 +35450,10 @@ package android.provider { method public final android.content.res.AssetFileDescriptor openTypedAssetFile(android.net.Uri, java.lang.String, android.os.Bundle, android.os.CancellationSignal) throws java.io.FileNotFoundException; method public android.content.res.AssetFileDescriptor openTypedDocument(java.lang.String, java.lang.String, android.os.Bundle, android.os.CancellationSignal) throws java.io.FileNotFoundException; method public final android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String); + method public final android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, android.os.CancellationSignal); + method public final android.database.Cursor query(android.net.Uri, java.lang.String[], android.os.Bundle, android.os.CancellationSignal); method public abstract android.database.Cursor queryChildDocuments(java.lang.String, java.lang.String[], java.lang.String) throws java.io.FileNotFoundException; + method public android.database.Cursor queryChildDocuments(java.lang.String, java.lang.String[], android.os.Bundle) throws java.io.FileNotFoundException; method public abstract android.database.Cursor queryDocument(java.lang.String, java.lang.String[]) throws java.io.FileNotFoundException; method public android.database.Cursor queryRecentDocuments(java.lang.String, java.lang.String[]) throws java.io.FileNotFoundException; method public abstract android.database.Cursor queryRoots(java.lang.String[]) throws java.io.FileNotFoundException; diff --git a/api/test-current.txt b/api/test-current.txt index 051dfbb1f37c..c2e9fbbb6f84 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -8225,9 +8225,14 @@ package android.content { field public static final java.lang.String EXTRA_SIZE = "android.content.extra.SIZE"; field public static final int NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS = 2; // 0x2 field public static final int NOTIFY_SYNC_TO_NETWORK = 1; // 0x1 - field public static final java.lang.String QUERY_ARG_SELECTION = "android:query-selection"; - field public static final java.lang.String QUERY_ARG_SELECTION_ARGS = "android:query-selection-args"; - field public static final java.lang.String QUERY_ARG_SORT_ORDER = "android:query-sort-order"; + field public static final java.lang.String QUERY_ARG_SORT_COLLATION = "android:query-sort-collation"; + field public static final java.lang.String QUERY_ARG_SORT_COLUMNS = "android:query-sort-columns"; + field public static final java.lang.String QUERY_ARG_SORT_DIRECTION = "android:query-sort-direction"; + field public static final java.lang.String QUERY_ARG_SQL_SELECTION = "android:query-sql-selection"; + field public static final java.lang.String QUERY_ARG_SQL_SELECTION_ARGS = "android:query-sql-selection-args"; + field public static final java.lang.String QUERY_ARG_SQL_SORT_ORDER = "android:query-sql-sort-order"; + field public static final int QUERY_SORT_DIRECTION_ASCENDING = 0; // 0x0 + field public static final int QUERY_SORT_DIRECTION_DESCENDING = 1; // 0x1 field public static final java.lang.String SCHEME_ANDROID_RESOURCE = "android.resource"; field public static final java.lang.String SCHEME_CONTENT = "content"; field public static final java.lang.String SCHEME_FILE = "file"; @@ -32802,7 +32807,10 @@ package android.provider { method public final android.content.res.AssetFileDescriptor openTypedAssetFile(android.net.Uri, java.lang.String, android.os.Bundle, android.os.CancellationSignal) throws java.io.FileNotFoundException; method public android.content.res.AssetFileDescriptor openTypedDocument(java.lang.String, java.lang.String, android.os.Bundle, android.os.CancellationSignal) throws java.io.FileNotFoundException; method public final android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String); + method public final android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, android.os.CancellationSignal); + method public final android.database.Cursor query(android.net.Uri, java.lang.String[], android.os.Bundle, android.os.CancellationSignal); method public abstract android.database.Cursor queryChildDocuments(java.lang.String, java.lang.String[], java.lang.String) throws java.io.FileNotFoundException; + method public android.database.Cursor queryChildDocuments(java.lang.String, java.lang.String[], android.os.Bundle) throws java.io.FileNotFoundException; method public abstract android.database.Cursor queryDocument(java.lang.String, java.lang.String[]) throws java.io.FileNotFoundException; method public android.database.Cursor queryRecentDocuments(java.lang.String, java.lang.String[]) throws java.io.FileNotFoundException; method public abstract android.database.Cursor queryRoots(java.lang.String[]) throws java.io.FileNotFoundException; diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index cda98e5b4c79..12f644214edf 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -915,7 +915,12 @@ public abstract class ContentProvider implements ComponentCallbacks2 { /** * Implement this to handle query requests from clients. - * This method can be called from multiple threads, as described in + * + * <p>Apps targeting {@link android.os.Build.VERSION_CODES#O} or higher should override + * {@link #query(Uri, String[], Bundle, CancellationSignal)} and provide a stub + * implementation of this method. + * + * <p>This method can be called from multiple threads, as described in * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html#Threads">Processes * and Threads</a>. * <p> @@ -974,7 +979,11 @@ public abstract class ContentProvider implements ComponentCallbacks2 { /** * Implement this to handle query requests from clients with support for cancellation. - * This method can be called from multiple threads, as described in + * + * <p>Apps targeting {@link android.os.Build.VERSION_CODES#O} or higher should override + * {@link #query(Uri, String[], Bundle, CancellationSignal)} instead of this method. + * + * <p>This method can be called from multiple threads, as described in * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html#Threads">Processes * and Threads</a>. * <p> @@ -1048,9 +1057,9 @@ public abstract class ContentProvider implements ComponentCallbacks2 { * {@link #query(Uri, String[], String, String[], String, CancellationSignal). * * <p>Traditional SQL arguments can be found in the bundle using the following keys: - * <li>{@link ContentResolver#QUERY_ARG_SELECTION} - * <li>{@link ContentResolver#QUERY_ARG_SELECTION_ARGS} - * <li>{@link ContentResolver#QUERY_ARG_SORT_ORDER} + * <li>{@link ContentResolver#QUERY_ARG_SQL_SELECTION} + * <li>{@link ContentResolver#QUERY_ARG_SQL_SELECTION_ARGS} + * <li>{@link ContentResolver#QUERY_ARG_SQL_SORT_ORDER} * * @see #query(Uri, String[], String, String[], String, CancellationSignal) for * implementation details. @@ -1071,12 +1080,21 @@ public abstract class ContentProvider implements ComponentCallbacks2 { public @Nullable Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal) { queryArgs = queryArgs != null ? queryArgs : Bundle.EMPTY; + + String sortClause = queryArgs.getString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER); + + // if client didn't explicitly supply and sql sort order argument, we try to build + // one from sort columns if present. + if (sortClause == null && queryArgs.containsKey(ContentResolver.QUERY_ARG_SORT_COLUMNS)) { + sortClause = ContentResolver.createSqlSortClause(queryArgs); + } + return query( uri, projection, - queryArgs.getString(ContentResolver.QUERY_ARG_SELECTION), - queryArgs.getStringArray(ContentResolver.QUERY_ARG_SELECTION_ARGS), - queryArgs.getString(ContentResolver.QUERY_ARG_SORT_ORDER), + queryArgs.getString(ContentResolver.QUERY_ARG_SQL_SELECTION), + queryArgs.getStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS), + sortClause, cancellationSignal); } diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index c4bf4f7d49a6..80556bde46c5 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -205,21 +205,111 @@ public abstract class ContentResolver { * Key for an SQL style selection string that may be present in the query Bundle argument * passed to {@link ContentProvider#query(Uri, String[], Bundle, CancellationSignal)} * when called by a legacy client. + * + * <p>Clients should never include user supplied values directly in the selection string, + * as this presents an avenue for SQL injection attacks. In lieu of this, a client + * should use standard placeholder notation to represent values in a selection string, + * then supply a corresponding value in {@value #QUERY_ARG_SQL_SELECTION_ARGS}. + * + * <p><b>Clients targeting Android O or higher are strongly encourage to use structured + * query arguments in lieu of opaque SQL query clauses.</b> See: + * {@link #QUERY_ARG_SORT_COLUMNS}, {@link #QUERY_ARG_SORT_DIRECTION}, and + * {@link #QUERY_ARG_SORT_COLLATION}. */ - public static final String QUERY_ARG_SELECTION = "android:query-selection"; + public static final String QUERY_ARG_SQL_SELECTION = "android:query-sql-selection"; /** - * Key for sql selection string arguments list. - * @see #QUERY_ARG_SELECTION + * Key for SQL selection string arguments list. + * + * <p>Clients should never include user supplied values directly in the selection string, + * as this presents an avenue for SQL injection attacks. In lieu of this, a client + * should use standard placeholder notation to represent values in a selection string, + * then supply a corresponding value in {@value #QUERY_ARG_SQL_SELECTION_ARGS}. + * + * <p><b>Clients targeting Android O or higher are strongly encourage to use structured + * query arguments in lieu of opaque SQL query clauses.</b> See: + * {@link #QUERY_ARG_SORT_COLUMNS}, {@link #QUERY_ARG_SORT_DIRECTION}, and + * {@link #QUERY_ARG_SORT_COLLATION}. */ - public static final String QUERY_ARG_SELECTION_ARGS = "android:query-selection-args"; + public static final String QUERY_ARG_SQL_SELECTION_ARGS = "android:query-sql-selection-args"; /** * Key for an SQL style sort string that may be present in the query Bundle argument * passed to {@link ContentProvider#query(Uri, String[], Bundle, CancellationSignal)} * when called by a legacy client. + * + * <p><b>Clients targeting Android O or higher are strongly encourage to use structured + * query arguments in lieu of opaque SQL query clauses.</b> See: + * {@link #QUERY_ARG_SORT_COLUMNS}, {@link #QUERY_ARG_SORT_DIRECTION}, and + * {@link #QUERY_ARG_SORT_COLLATION}. + */ + public static final String QUERY_ARG_SQL_SORT_ORDER = "android:query-sql-sort-order"; + + /** + * Identifies the list columns against which to sort results. + * + * <p>Columns present in this list must also be included in the projection + * supplied to {@link ContentResolver#query(Uri, String[], Bundle, CancellationSignal)}. + * + * <p>Apps targeting {@link android.os.Build.VERSION_CODES#O} or higher are strongly + * encouraged to include an entry in Cursor extras under this same key as an indication + * to the client that column sorting was honored. + * + * <p>QUERY_SORT* values are exclusive from QUERY_ARG_SQL* arguments. + * When any QUERY_SORT arguments are present, any QUERY_ARG_SQL* values will be ignored. + */ + public static final String QUERY_ARG_SORT_COLUMNS = "android:query-sort-columns"; + + /** + * Specifies desired sort order. When unspecified a provider may provide a default + * sort direction, or choose to return unsorted results. + * + * <p>Apps targeting {@link android.os.Build.VERSION_CODES#O} or higher are strongly + * encouraged to include an entry in Cursor extras under this same key as an indication + * to the client that sort direction was honored. + * + * @see #QUERY_SORT_DIRECTION_ASCENDING + * @see #QUERY_SORT_DIRECTION_DESCENDING + */ + public static final String QUERY_ARG_SORT_DIRECTION = "android:query-sort-direction"; + + /** + * Allows client to specify a hint to the provider as to which collation + * to use when sorting text values. + * + * <p>Providers may provide their own collators. When selecting a custom collator + * the value will be determined by the Provider. + * + * <p>apps targeting {@link android.os.Build.VERSION_CODES#O} or higher are strongly + * encouraged to include an entry in Cursor extras under this same key as an indication + * to the client that collation was honored. + * + * @see #QUERY_COLLATOR_MODE_NOCASE */ - public static final String QUERY_ARG_SORT_ORDER = "android:query-sort-order"; + public static final String QUERY_ARG_SORT_COLLATION = "android:query-sort-collation"; + + /** @hide */ + @IntDef(flag = false, value = { + QUERY_SORT_DIRECTION_ASCENDING, + QUERY_SORT_DIRECTION_DESCENDING + }) + @Retention(RetentionPolicy.SOURCE) + public @interface SortDirection {} + public static final int QUERY_SORT_DIRECTION_ASCENDING = 0; + public static final int QUERY_SORT_DIRECTION_DESCENDING = 1; + + /** + * @see {@link java.text.Collector} for details on respective collation strength. + * @hide + */ + @IntDef(flag = false, value = { + java.text.Collator.PRIMARY, + java.text.Collator.SECONDARY, + java.text.Collator.TERTIARY, + java.text.Collator.IDENTICAL + }) + @Retention(RetentionPolicy.SOURCE) + public @interface QueryCollator {} /** * This is the Android platform's base MIME type for a content: URI @@ -2685,8 +2775,8 @@ public abstract class ContentResolver { EventLogTags.CONTENT_QUERY_SAMPLE, uri.toString(), projectionBuffer.toString(), - queryArgs.getString(QUERY_ARG_SELECTION, ""), - queryArgs.getString(QUERY_ARG_SORT_ORDER, ""), + queryArgs.getString(QUERY_ARG_SQL_SELECTION, ""), + queryArgs.getString(QUERY_ARG_SQL_SORT_ORDER, ""), durationMillis, blockingPackage != null ? blockingPackage : "", samplePercent); @@ -2815,14 +2905,54 @@ public abstract class ContentResolver { Bundle queryArgs = new Bundle(); if (selection != null) { - queryArgs.putString(QUERY_ARG_SELECTION, selection); + queryArgs.putString(QUERY_ARG_SQL_SELECTION, selection); } if (selectionArgs != null) { - queryArgs.putStringArray(QUERY_ARG_SELECTION_ARGS, selectionArgs); + queryArgs.putStringArray(QUERY_ARG_SQL_SELECTION_ARGS, selectionArgs); } if (sortOrder != null) { - queryArgs.putString(QUERY_ARG_SORT_ORDER, sortOrder); + queryArgs.putString(QUERY_ARG_SQL_SORT_ORDER, sortOrder); } return queryArgs; } + + /** + * Returns structured sort args formatted as an SQL sort clause. + * + * Collator clauses are not included as column information is unknown, and + * collate clauses should only be included on text fields. + * + * TODO: Should we explicitly validate that colums are present in the projection? + * + * @hide + */ + public static String createSqlSortClause(Bundle queryArgs) { + String[] columns = queryArgs.getStringArray(QUERY_ARG_SORT_COLUMNS); + if (columns == null || columns.length == 0) { + throw new IllegalArgumentException("Can't create sort clause without columns."); + } + + String query = TextUtils.join(", ", columns); + + switch (queryArgs.getInt( + QUERY_ARG_SORT_DIRECTION, Integer.MIN_VALUE)) { + case QUERY_SORT_DIRECTION_ASCENDING: + query += " ASC"; + break; + case QUERY_SORT_DIRECTION_DESCENDING: + query += " DESC"; + break; + default: + throw new IllegalArgumentException("Unsupported sort direction value." + + " See ContentResolver documentation for details."); + } + + // Interpret PRIMARY collation strength as no-case collation. + int collation = queryArgs.getInt( + ContentResolver.QUERY_ARG_SORT_COLLATION, java.text.Collator.IDENTICAL); + if (collation == java.text.Collator.PRIMARY || collation == java.text.Collator.SECONDARY) { + query += " COLLATE NOCASE"; + } + return query; + } } diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java index 584f5fe494e1..8bc03ee622a2 100644 --- a/core/java/android/provider/DocumentsProvider.java +++ b/core/java/android/provider/DocumentsProvider.java @@ -52,6 +52,7 @@ import android.graphics.Point; import android.net.Uri; import android.os.Bundle; import android.os.CancellationSignal; +import android.os.OperationCanceledException; import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor.OnCloseListener; import android.provider.DocumentsContract.Document; @@ -416,6 +417,9 @@ public abstract class DocumentsProvider extends ContentProvider { * must only return immediate descendants, as additional queries will be * issued to recursively explore the tree. * <p> + * Apps targeting {@link android.os.Build.VERSION_CODES#O} or higher + * should override {@link #queryChildDocuments(String, String[], Bundle)}. + * <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 @@ -450,10 +454,53 @@ public abstract class DocumentsProvider extends ContentProvider { String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException; + /** + * Override this method to return the children documents contained + * in the requested directory. This must return immediate descendants only. + * + * <p>If your provider is cloud-based, and you have data cached + * locally, you may return the local data immediately, setting + * {@link DocumentsContract#EXTRA_LOADING} on Cursor extras 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 return a Cursor with extras, you need to + * extend and override {@link Cursor#getExtras()}. + * + * <p>To support change notifications, you must + * {@link Cursor#setNotificationUri(ContentResolver, Uri)} with a relevant + * Uri, such as + * {@link DocumentsContract#buildChildDocumentsUri(String, String)}. Then + * you can call {@link ContentResolver#notifyChange(Uri, + * android.database.ContentObserver, boolean)} with that Uri to send change + * notifications. + * + * @param parentDocumentId the directory to return children for. + * @param projection list of {@link Document} columns to put into the + * cursor. If {@code null} all supported columns should be + * included. + * @param queryArgs Bundle containing sorting information or other + * argument useful to the provider. If no sorting + * information is available, default sorting + * will be used, which may be unordered. See + * {@link ContentResolver#QUERY_ARG_SORT_COLUMNS} for + * details. + * + * @see DocumentsContract#EXTRA_LOADING + * @see DocumentsContract#EXTRA_INFO + * @see DocumentsContract#EXTRA_ERROR + */ + public Cursor queryChildDocuments( + String parentDocumentId, @Nullable String[] projection, @Nullable Bundle queryArgs) + throws FileNotFoundException { + + return queryChildDocuments( + parentDocumentId, projection, getSortClause(queryArgs)); + } + /** {@hide} */ @SuppressWarnings("unused") public Cursor queryChildDocumentsForManage( - String parentDocumentId, String[] projection, String sortOrder) + String parentDocumentId, @Nullable String[] projection, @Nullable String sortOrder) throws FileNotFoundException { throw new UnsupportedOperationException("Manage not supported"); } @@ -594,6 +641,22 @@ public abstract class DocumentsProvider extends ContentProvider { throw new FileNotFoundException("The requested MIME type is not supported."); } + @Override + public final Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + // As of Android-O, ContentProvider#query (w/ bundle arg) is the primary + // transport method. We override that, and don't ever delegate to this method. + throw new UnsupportedOperationException("Pre-Android-O query format not supported."); + } + + @Override + public final Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal) { + // As of Android-O, ContentProvider#query (w/ bundle arg) is the primary + // transport method. We override that, and don't ever delegate to this metohd. + throw new UnsupportedOperationException("Pre-Android-O query format not supported."); + } + /** * Implementation is provided by the parent class. Cannot be overriden. * @@ -604,8 +667,8 @@ public abstract class DocumentsProvider extends ContentProvider { * @see #querySearchDocuments(String, String, String[]) */ @Override - public final Cursor query(Uri uri, String[] projection, String selection, - String[] selectionArgs, String sortOrder) { + public final Cursor query( + Uri uri, String[] projection, Bundle queryArgs, CancellationSignal cancellationSignal) { try { switch (mMatcher.match(uri)) { case MATCH_ROOTS: @@ -623,10 +686,13 @@ public abstract class DocumentsProvider extends ContentProvider { case MATCH_CHILDREN_TREE: enforceTree(uri); if (DocumentsContract.isManageMode(uri)) { + // TODO: Update "ForManage" variant to support query args. return queryChildDocumentsForManage( - getDocumentId(uri), projection, sortOrder); + getDocumentId(uri), + projection, + getSortClause(queryArgs)); } else { - return queryChildDocuments(getDocumentId(uri), projection, sortOrder); + return queryChildDocuments(getDocumentId(uri), projection, queryArgs); } default: throw new UnsupportedOperationException("Unsupported Uri " + uri); @@ -637,6 +703,17 @@ public abstract class DocumentsProvider extends ContentProvider { } } + private static @Nullable String getSortClause(@Nullable Bundle queryArgs) { + queryArgs = queryArgs != null ? queryArgs : Bundle.EMPTY; + String sortClause = queryArgs.getString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER); + + if (sortClause == null && queryArgs.containsKey(ContentResolver.QUERY_ARG_SORT_COLUMNS)) { + sortClause = ContentResolver.createSqlSortClause(queryArgs); + } + + return sortClause; + } + /** * Implementation is provided by the parent class. Cannot be overriden. * diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java index cbd295e1f85e..22a5b7ffd520 100644 --- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java +++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java @@ -687,7 +687,7 @@ public class ExternalStorageProvider extends DocumentsProvider { parent = mRoots.get(rootId).path; } - final LinkedList<File> pending = new LinkedList<File>(); + final LinkedList<File> pending = new LinkedList<>(); pending.add(parent); while (!pending.isEmpty() && result.getCount() < 24) { final File file = pending.removeFirst(); diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java index ef2e0a58903c..a9d35e1dedf8 100644 --- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java +++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java @@ -162,7 +162,7 @@ public class MtpDocumentsProviderTest extends AndroidTestCase { assertEquals(0, openedDevice.length); } // Device is opened automatically when querying its children. - try (final Cursor cursor = mProvider.queryChildDocuments("1", null, null)) {} + try (final Cursor cursor = mProvider.queryChildDocuments("1", null, (String) null)) {} { final MtpDeviceRecord[] openedDevice = mProvider.getOpenedDeviceRecordsCache(); @@ -412,7 +412,7 @@ public class MtpDocumentsProviderTest extends AndroidTestCase { .build() }); - final Cursor cursor = mProvider.queryChildDocuments("1", null, null); + final Cursor cursor = mProvider.queryChildDocuments("1", null, (String) null); assertEquals(1, cursor.getCount()); assertTrue(cursor.moveToNext()); @@ -429,7 +429,7 @@ public class MtpDocumentsProviderTest extends AndroidTestCase { public void testQueryChildDocuments_cursorError() throws Exception { setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); try { - mProvider.queryChildDocuments("1", null, null); + mProvider.queryChildDocuments("1", null, (String) null); fail(); } catch (FileNotFoundException error) {} } @@ -438,7 +438,7 @@ public class MtpDocumentsProviderTest extends AndroidTestCase { setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY); setupRoots(0, new MtpRoot[] { new MtpRoot(0, 0, "Storage", 1000, 1000, "") }); mMtpManager.setObjectHandles(0, 0, -1, new int[] { 1 }); - try (final Cursor cursor = mProvider.queryChildDocuments("1", null, null)) { + try (final Cursor cursor = mProvider.queryChildDocuments("1", null, (String) null)) { assertEquals(0, cursor.getCount()); assertFalse(cursor.getExtras().getBoolean(DocumentsContract.EXTRA_LOADING)); } @@ -590,7 +590,7 @@ public class MtpDocumentsProviderTest extends AndroidTestCase { assertEquals(1, cursor.getCount()); } - try (final Cursor cursor = mProvider.queryChildDocuments("1", null, null)) { + try (final Cursor cursor = mProvider.queryChildDocuments("1", null, (String) null)) { assertEquals(0, cursor.getCount()); assertEquals( "error_busy_device", @@ -611,7 +611,7 @@ public class MtpDocumentsProviderTest extends AndroidTestCase { assertEquals(1, cursor.getCount()); } - try (final Cursor cursor = mProvider.queryChildDocuments("1", null, null)) { + try (final Cursor cursor = mProvider.queryChildDocuments("1", null, (String) null)) { assertEquals(0, cursor.getCount()); assertEquals( "error_locked_device", @@ -663,7 +663,7 @@ public class MtpDocumentsProviderTest extends AndroidTestCase { try (final Cursor cursor = mProvider.queryChildDocuments( String.valueOf(documentIdOffset + i), strings(Document.COLUMN_DOCUMENT_ID, Document.COLUMN_DISPLAY_NAME), - null)) { + (String) null)) { assertEquals(1, cursor.getCount()); cursor.moveToNext(); assertEquals(String.valueOf(documentIdOffset + i + 1), cursor.getString(0)); @@ -684,7 +684,7 @@ public class MtpDocumentsProviderTest extends AndroidTestCase { try (final Cursor cursor = mProvider.queryChildDocuments( String.valueOf(documentIdOffset + i), strings(Document.COLUMN_DOCUMENT_ID), - null)) { + (String) null)) { assertEquals(1, cursor.getCount()); cursor.moveToNext(); assertEquals(String.valueOf(documentIdOffset + i + 1), cursor.getString(0)); @@ -758,7 +758,7 @@ public class MtpDocumentsProviderTest extends AndroidTestCase { mProvider.resumeRootScanner(); mResolver.waitForNotification(ROOTS_URI, 1); try (final Cursor cursor = mProvider.queryChildDocuments( - "1", strings(Document.COLUMN_DOCUMENT_ID), null)) { + "1", strings(Document.COLUMN_DOCUMENT_ID), (String) null)) { assertEquals(1, cursor.getCount()); cursor.moveToNext(); assertEquals("3", cursor.getString(0)); @@ -917,7 +917,9 @@ public class MtpDocumentsProviderTest extends AndroidTestCase { } mMtpManager.setObjectHandles(deviceId, storageId, parentHandle, handles); return getStrings(mProvider.queryChildDocuments( - parentDocumentId, strings(DocumentsContract.Document.COLUMN_DOCUMENT_ID), null)); + parentDocumentId, + strings(DocumentsContract.Document.COLUMN_DOCUMENT_ID), + (String) null)); } static class HierarchyDocument { |