Add structured sort data to ContentResolver.query.

Update DocumentsProvider to override
    ContentProvider#query(Uri, String[], Bundle, CancellationSignal);
Added an otherwise unneeded import to pass doc check
    on DocumentsProvider.

Bug: 30927484
Change-Id: I295c21f53901d567455286f22439f21d22a8a25a
Test: Build and run. Test from DocsUi.
diff --git a/api/current.txt b/api/current.txt
index a4402bb..1cd1c44 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -8197,9 +8197,14 @@
     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";
@@ -32556,7 +32561,10 @@
     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 2ec5643..97d9aea 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -8541,9 +8541,14 @@
     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";
@@ -35306,7 +35311,10 @@
     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 7e05b78..ae74749 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -8220,9 +8220,14 @@
     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";
@@ -32669,7 +32674,10 @@
     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 cda98e5..12f6442 100644
--- a/core/java/android/content/ContentProvider.java
+++ b/core/java/android/content/ContentProvider.java
@@ -915,7 +915,12 @@
 
     /**
      * 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 @@
 
     /**
      * 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 @@
      * {@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 @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 c4bf4f7..80556bd 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -205,21 +205,111 @@
      * 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_SORT_ORDER = "android:query-sort-order";
+    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_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 @@
             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 @@
 
         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 584f5fe..8bc03ee 100644
--- a/core/java/android/provider/DocumentsProvider.java
+++ b/core/java/android/provider/DocumentsProvider.java
@@ -52,6 +52,7 @@
 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 @@
      * 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 @@
             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 @@
         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 @@
      * @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 @@
                 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 @@
         }
     }
 
+    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 8cf375a..1c08ea5 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -670,7 +670,7 @@
             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 ef2e0a5..a9d35e1 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 @@
             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 @@
                                 .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 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 @@
         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 @@
             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 @@
             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 @@
             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 @@
             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 @@
         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 @@
         }
         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 {