Loading src/com/android/documentsui/services/CopyJob.java +88 −4 Original line number Diff line number Diff line Loading @@ -38,9 +38,12 @@ import android.content.ContentProviderClient; import android.content.Context; import android.content.Intent; import android.content.res.AssetFileDescriptor; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.os.CancellationSignal; import android.os.Handler; import android.os.Looper; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.provider.DocumentsContract; Loading Loading @@ -75,6 +78,8 @@ class CopyJob extends ResolvedResourcesJob { private static final String TAG = "CopyJob"; private static final long LOADING_TIMEOUT = 60000; // 1 min final ArrayList<DocumentInfo> convertedFiles = new ArrayList<>(); private long mStartTime = -1; Loading Loading @@ -460,10 +465,9 @@ class CopyJob extends ResolvedResourcesJob { Cursor cursor = null; boolean success = true; // Iterate over srcs in the directory; copy to the destination directory. final Uri queryUri = buildChildDocumentsUri(srcDir.authority, srcDir.documentId); try { try { cursor = getClient(srcDir).query(queryUri, queryColumns, null, null, null); cursor = queryChildren(srcDir, queryColumns); } catch (RemoteException | RuntimeException e) { Metrics.logFileOperationFailure( appContext, Metrics.SUBFILEOP_QUERY_CHILDREN, srcDir.derivedUri); Loading Loading @@ -669,7 +673,6 @@ class CopyJob extends ResolvedResourcesJob { long calculateFileSizesRecursively( ContentProviderClient client, Uri uri) throws ResourceException { final String authority = uri.getAuthority(); final Uri queryUri = buildChildDocumentsUri(authority, getDocumentId(uri)); final String queryColumns[] = new String[] { Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Loading @@ -679,7 +682,7 @@ class CopyJob extends ResolvedResourcesJob { long result = 0; Cursor cursor = null; try { cursor = client.query(queryUri, queryColumns, null, null, null); cursor = queryChildren(client, uri, queryColumns); while (cursor.moveToNext() && !isCanceled()) { if (Document.MIME_TYPE_DIR.equals( getCursorString(cursor, Document.COLUMN_MIME_TYPE))) { Loading @@ -703,6 +706,69 @@ class CopyJob extends ResolvedResourcesJob { return result; } /** * Queries children documents. * * SAF allows {@link DocumentsContract#EXTRA_LOADING} in {@link Cursor#getExtras()} to indicate * there are more data to be loaded. Wait until {@link DocumentsContract#EXTRA_LOADING} is * false and then return the cursor. * * @param srcDir the directory whose children are being loading * @param queryColumns columns of metadata to load * @return cursor of all children documents * @throws RemoteException when the remote throws or waiting for update times out */ private Cursor queryChildren(DocumentInfo srcDir, String[] queryColumns) throws RemoteException { try (final ContentProviderClient client = getClient(srcDir)) { return queryChildren(client, srcDir.derivedUri, queryColumns); } } /** * Queries children documents. * * SAF allows {@link DocumentsContract#EXTRA_LOADING} in {@link Cursor#getExtras()} to indicate * there are more data to be loaded. Wait until {@link DocumentsContract#EXTRA_LOADING} is * false and then return the cursor. * * @param client the {@link ContentProviderClient} to use to query children * @param dirDocUri the document Uri of the directory whose children are being loaded * @param queryColumns columns of metadata to load * @return cursor of all children documents * @throws RemoteException when the remote throws or waiting for update times out */ private Cursor queryChildren(ContentProviderClient client, Uri dirDocUri, String[] queryColumns) throws RemoteException { // TODO (b/34459983): Optimize this performance by processing partial result first while provider is loading // more data. Note we need to skip size calculation to achieve it. final Uri queryUri = buildChildDocumentsUri(dirDocUri.getAuthority(), getDocumentId(dirDocUri)); Cursor cursor = client.query( queryUri, queryColumns, (String) null, null, null); while (cursor.getExtras().getBoolean(DocumentsContract.EXTRA_LOADING)) { cursor.registerContentObserver(new DirectoryChildrenObserver(queryUri)); try { long start = System.currentTimeMillis(); synchronized (queryUri) { queryUri.wait(LOADING_TIMEOUT); } if (System.currentTimeMillis() - start > LOADING_TIMEOUT) { // Timed out throw new RemoteException("Timed out waiting on update for " + queryUri); } } catch (InterruptedException e) { // Should never happen throw new RuntimeException(e); } // Make another query cursor = client.query( queryUri, queryColumns, (String) null, null, null); } return cursor; } /** * Returns true if {@code doc} is a descendant of {@code parentDoc}. * @throws ResourceException Loading Loading @@ -733,4 +799,22 @@ class CopyJob extends ResolvedResourcesJob { .append("}") .toString(); } private static class DirectoryChildrenObserver extends ContentObserver { private final Object mNotifier; private DirectoryChildrenObserver(Object notifier) { super(new Handler(Looper.getMainLooper())); assert(notifier != null); mNotifier = notifier; } @Override public void onChange(boolean selfChange, Uri uri) { synchronized (mNotifier) { mNotifier.notify(); } } } } tests/common/com/android/documentsui/DocumentsProviderHelper.java +5 −0 Original line number Diff line number Diff line Loading @@ -304,4 +304,9 @@ public class DocumentsProviderHelper { return DocumentsContract.buildDocumentUri(mAuthority, documentId); } public void setLoadingDuration(long duration) throws RemoteException { final Bundle extra = new Bundle(); extra.putLong(DocumentsContract.EXTRA_LOADING, duration); mClient.call("setLoadingDuration", null, extra); } } tests/common/com/android/documentsui/StubProvider.java +38 −18 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.documentsui; import android.content.ContentResolver; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.ProviderInfo; Loading @@ -25,10 +26,7 @@ import android.database.MatrixCursor; import android.database.MatrixCursor.RowBuilder; import android.graphics.Point; import android.net.Uri; import android.os.Bundle; import android.os.CancellationSignal; import android.os.FileUtils; import android.os.ParcelFileDescriptor; import android.os.*; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.provider.DocumentsContract.Root; Loading Loading @@ -91,6 +89,7 @@ public class StubProvider extends DocumentsProvider { private String mAuthority = DEFAULT_AUTHORITY; private SharedPreferences mPrefs; private Set<String> mSimulateReadErrorIds = new HashSet<>(); private long mLoadingDuration = 0; @Override public void attachInfo(Context context, ProviderInfo info) { Loading Loading @@ -134,6 +133,8 @@ public class StubProvider extends DocumentsProvider { mStorage.put(rootInfo.document.documentId, rootInfo.document); mRoots.put(rootId, rootInfo); } mLoadingDuration = 0; } /** Loading Loading @@ -225,6 +226,21 @@ public class StubProvider extends DocumentsProvider { @Override public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException { if (mLoadingDuration > 0) { final Uri notifyUri = DocumentsContract.buildDocumentUri(mAuthority, parentDocumentId); final ContentResolver resolver = getContext().getContentResolver(); new Handler(Looper.getMainLooper()).postDelayed( () -> resolver.notifyChange(notifyUri, null, false), mLoadingDuration); mLoadingDuration = 0; MatrixCursor cursor = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION); Bundle bundle = new Bundle(); bundle.putBoolean(DocumentsContract.EXTRA_LOADING, true); cursor.setExtras(bundle); cursor.setNotificationUri(resolver, notifyUri); return cursor; } else { final StubDocument parentDocument = mStorage.get(parentDocumentId); if (parentDocument == null || parentDocument.file.isFile()) { throw new FileNotFoundException(); Loading @@ -242,6 +258,7 @@ public class StubProvider extends DocumentsProvider { } return result; } } @Override public Cursor queryRecentDocuments(String rootId, String[] projection) Loading Loading @@ -489,6 +506,9 @@ public class StubProvider extends DocumentsProvider { return null; case "createDocumentWithFlags": return dispatchCreateDocumentWithFlags(extras); case "setLoadingDuration": mLoadingDuration = extras.getLong(DocumentsContract.EXTRA_LOADING); return null; } return null; Loading tests/unit/com/android/documentsui/services/CopyJobTest.java +5 −0 Original line number Diff line number Diff line Loading @@ -66,6 +66,11 @@ public class CopyJobTest extends AbstractCopyJobTest<CopyJob> { runCopyDirRecursivelyTest(); } public void testCopyDirRecursively_loadingInFirstCursor() throws Exception { mDocs.setLoadingDuration(500); testCopyDirRecursively(); } public void testNoCopyDirToSelf() throws Exception { runNoCopyDirToSelfTest(); } Loading tests/unit/com/android/documentsui/services/MoveJobTest.java +5 −0 Original line number Diff line number Diff line Loading @@ -102,6 +102,11 @@ public class MoveJobTest extends AbstractCopyJobTest<MoveJob> { mDocs.assertChildCount(mSrcRoot, 0); } public void testMoveDirRecursively_loadingInFirstCursor() throws Exception { mDocs.setLoadingDuration(500); testMoveDirRecursively(); } public void testNoMoveDirToSelf() throws Exception { runNoCopyDirToSelfTest(); Loading Loading
src/com/android/documentsui/services/CopyJob.java +88 −4 Original line number Diff line number Diff line Loading @@ -38,9 +38,12 @@ import android.content.ContentProviderClient; import android.content.Context; import android.content.Intent; import android.content.res.AssetFileDescriptor; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.os.CancellationSignal; import android.os.Handler; import android.os.Looper; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.provider.DocumentsContract; Loading Loading @@ -75,6 +78,8 @@ class CopyJob extends ResolvedResourcesJob { private static final String TAG = "CopyJob"; private static final long LOADING_TIMEOUT = 60000; // 1 min final ArrayList<DocumentInfo> convertedFiles = new ArrayList<>(); private long mStartTime = -1; Loading Loading @@ -460,10 +465,9 @@ class CopyJob extends ResolvedResourcesJob { Cursor cursor = null; boolean success = true; // Iterate over srcs in the directory; copy to the destination directory. final Uri queryUri = buildChildDocumentsUri(srcDir.authority, srcDir.documentId); try { try { cursor = getClient(srcDir).query(queryUri, queryColumns, null, null, null); cursor = queryChildren(srcDir, queryColumns); } catch (RemoteException | RuntimeException e) { Metrics.logFileOperationFailure( appContext, Metrics.SUBFILEOP_QUERY_CHILDREN, srcDir.derivedUri); Loading Loading @@ -669,7 +673,6 @@ class CopyJob extends ResolvedResourcesJob { long calculateFileSizesRecursively( ContentProviderClient client, Uri uri) throws ResourceException { final String authority = uri.getAuthority(); final Uri queryUri = buildChildDocumentsUri(authority, getDocumentId(uri)); final String queryColumns[] = new String[] { Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Loading @@ -679,7 +682,7 @@ class CopyJob extends ResolvedResourcesJob { long result = 0; Cursor cursor = null; try { cursor = client.query(queryUri, queryColumns, null, null, null); cursor = queryChildren(client, uri, queryColumns); while (cursor.moveToNext() && !isCanceled()) { if (Document.MIME_TYPE_DIR.equals( getCursorString(cursor, Document.COLUMN_MIME_TYPE))) { Loading @@ -703,6 +706,69 @@ class CopyJob extends ResolvedResourcesJob { return result; } /** * Queries children documents. * * SAF allows {@link DocumentsContract#EXTRA_LOADING} in {@link Cursor#getExtras()} to indicate * there are more data to be loaded. Wait until {@link DocumentsContract#EXTRA_LOADING} is * false and then return the cursor. * * @param srcDir the directory whose children are being loading * @param queryColumns columns of metadata to load * @return cursor of all children documents * @throws RemoteException when the remote throws or waiting for update times out */ private Cursor queryChildren(DocumentInfo srcDir, String[] queryColumns) throws RemoteException { try (final ContentProviderClient client = getClient(srcDir)) { return queryChildren(client, srcDir.derivedUri, queryColumns); } } /** * Queries children documents. * * SAF allows {@link DocumentsContract#EXTRA_LOADING} in {@link Cursor#getExtras()} to indicate * there are more data to be loaded. Wait until {@link DocumentsContract#EXTRA_LOADING} is * false and then return the cursor. * * @param client the {@link ContentProviderClient} to use to query children * @param dirDocUri the document Uri of the directory whose children are being loaded * @param queryColumns columns of metadata to load * @return cursor of all children documents * @throws RemoteException when the remote throws or waiting for update times out */ private Cursor queryChildren(ContentProviderClient client, Uri dirDocUri, String[] queryColumns) throws RemoteException { // TODO (b/34459983): Optimize this performance by processing partial result first while provider is loading // more data. Note we need to skip size calculation to achieve it. final Uri queryUri = buildChildDocumentsUri(dirDocUri.getAuthority(), getDocumentId(dirDocUri)); Cursor cursor = client.query( queryUri, queryColumns, (String) null, null, null); while (cursor.getExtras().getBoolean(DocumentsContract.EXTRA_LOADING)) { cursor.registerContentObserver(new DirectoryChildrenObserver(queryUri)); try { long start = System.currentTimeMillis(); synchronized (queryUri) { queryUri.wait(LOADING_TIMEOUT); } if (System.currentTimeMillis() - start > LOADING_TIMEOUT) { // Timed out throw new RemoteException("Timed out waiting on update for " + queryUri); } } catch (InterruptedException e) { // Should never happen throw new RuntimeException(e); } // Make another query cursor = client.query( queryUri, queryColumns, (String) null, null, null); } return cursor; } /** * Returns true if {@code doc} is a descendant of {@code parentDoc}. * @throws ResourceException Loading Loading @@ -733,4 +799,22 @@ class CopyJob extends ResolvedResourcesJob { .append("}") .toString(); } private static class DirectoryChildrenObserver extends ContentObserver { private final Object mNotifier; private DirectoryChildrenObserver(Object notifier) { super(new Handler(Looper.getMainLooper())); assert(notifier != null); mNotifier = notifier; } @Override public void onChange(boolean selfChange, Uri uri) { synchronized (mNotifier) { mNotifier.notify(); } } } }
tests/common/com/android/documentsui/DocumentsProviderHelper.java +5 −0 Original line number Diff line number Diff line Loading @@ -304,4 +304,9 @@ public class DocumentsProviderHelper { return DocumentsContract.buildDocumentUri(mAuthority, documentId); } public void setLoadingDuration(long duration) throws RemoteException { final Bundle extra = new Bundle(); extra.putLong(DocumentsContract.EXTRA_LOADING, duration); mClient.call("setLoadingDuration", null, extra); } }
tests/common/com/android/documentsui/StubProvider.java +38 −18 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.documentsui; import android.content.ContentResolver; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.ProviderInfo; Loading @@ -25,10 +26,7 @@ import android.database.MatrixCursor; import android.database.MatrixCursor.RowBuilder; import android.graphics.Point; import android.net.Uri; import android.os.Bundle; import android.os.CancellationSignal; import android.os.FileUtils; import android.os.ParcelFileDescriptor; import android.os.*; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.provider.DocumentsContract.Root; Loading Loading @@ -91,6 +89,7 @@ public class StubProvider extends DocumentsProvider { private String mAuthority = DEFAULT_AUTHORITY; private SharedPreferences mPrefs; private Set<String> mSimulateReadErrorIds = new HashSet<>(); private long mLoadingDuration = 0; @Override public void attachInfo(Context context, ProviderInfo info) { Loading Loading @@ -134,6 +133,8 @@ public class StubProvider extends DocumentsProvider { mStorage.put(rootInfo.document.documentId, rootInfo.document); mRoots.put(rootId, rootInfo); } mLoadingDuration = 0; } /** Loading Loading @@ -225,6 +226,21 @@ public class StubProvider extends DocumentsProvider { @Override public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException { if (mLoadingDuration > 0) { final Uri notifyUri = DocumentsContract.buildDocumentUri(mAuthority, parentDocumentId); final ContentResolver resolver = getContext().getContentResolver(); new Handler(Looper.getMainLooper()).postDelayed( () -> resolver.notifyChange(notifyUri, null, false), mLoadingDuration); mLoadingDuration = 0; MatrixCursor cursor = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION); Bundle bundle = new Bundle(); bundle.putBoolean(DocumentsContract.EXTRA_LOADING, true); cursor.setExtras(bundle); cursor.setNotificationUri(resolver, notifyUri); return cursor; } else { final StubDocument parentDocument = mStorage.get(parentDocumentId); if (parentDocument == null || parentDocument.file.isFile()) { throw new FileNotFoundException(); Loading @@ -242,6 +258,7 @@ public class StubProvider extends DocumentsProvider { } return result; } } @Override public Cursor queryRecentDocuments(String rootId, String[] projection) Loading Loading @@ -489,6 +506,9 @@ public class StubProvider extends DocumentsProvider { return null; case "createDocumentWithFlags": return dispatchCreateDocumentWithFlags(extras); case "setLoadingDuration": mLoadingDuration = extras.getLong(DocumentsContract.EXTRA_LOADING); return null; } return null; Loading
tests/unit/com/android/documentsui/services/CopyJobTest.java +5 −0 Original line number Diff line number Diff line Loading @@ -66,6 +66,11 @@ public class CopyJobTest extends AbstractCopyJobTest<CopyJob> { runCopyDirRecursivelyTest(); } public void testCopyDirRecursively_loadingInFirstCursor() throws Exception { mDocs.setLoadingDuration(500); testCopyDirRecursively(); } public void testNoCopyDirToSelf() throws Exception { runNoCopyDirToSelfTest(); } Loading
tests/unit/com/android/documentsui/services/MoveJobTest.java +5 −0 Original line number Diff line number Diff line Loading @@ -102,6 +102,11 @@ public class MoveJobTest extends AbstractCopyJobTest<MoveJob> { mDocs.assertChildCount(mSrcRoot, 0); } public void testMoveDirRecursively_loadingInFirstCursor() throws Exception { mDocs.setLoadingDuration(500); testMoveDirRecursively(); } public void testNoMoveDirToSelf() throws Exception { runNoCopyDirToSelfTest(); Loading