diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java index 88eeb49ec1691..117bb01b1fece 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java +++ b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java @@ -36,6 +36,7 @@ import android.os.Handler; import android.os.SystemClock; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Root; +import android.provider.DocumentsProvider; import android.support.annotation.VisibleForTesting; import android.util.Log; @@ -350,11 +351,20 @@ public class RootsCache { * waiting for all the other roots to come back. */ public RootInfo getRootOneshot(String authority, String rootId) { + return getRootOneshot(authority, rootId, false); + } + + /** + * Return the requested {@link RootInfo}, but only loading the roots of the requested authority. + * It always fetches from {@link DocumentsProvider} if forceRefresh is true, which is used to + * get the most up-to-date free space before starting copy operations. + */ + public RootInfo getRootOneshot(String authority, String rootId, boolean forceRefresh) { synchronized (mLock) { - RootInfo root = getRootLocked(authority, rootId); + RootInfo root = forceRefresh ? null : getRootLocked(authority, rootId); if (root == null) { - mRoots.putAll(authority, - loadRootsForAuthority(mContext.getContentResolver(), authority, false)); + mRoots.putAll(authority, loadRootsForAuthority( + mContext.getContentResolver(), authority, forceRefresh)); root = getRootLocked(authority, rootId); } return root; diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java b/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java index 390656cf89865..54ccc2a29f340 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java +++ b/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java @@ -51,9 +51,11 @@ import android.text.format.DateUtils; import android.util.Log; import android.webkit.MimeTypeMap; -import com.android.documentsui.UrisSupplier; +import com.android.documentsui.DocumentsApplication; import com.android.documentsui.Metrics; import com.android.documentsui.R; +import com.android.documentsui.RootsCache; +import com.android.documentsui.UrisSupplier; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.DocumentStack; import com.android.documentsui.model.RootInfo; @@ -210,7 +212,6 @@ class CopyJob extends Job { @Override boolean setUp() { - try { buildDocumentList(); } catch (ResourceException e) { @@ -218,6 +219,7 @@ class CopyJob extends Job { return false; } + // Check if user has canceled this task. if (isCanceled()) { return false; } @@ -229,7 +231,15 @@ class CopyJob extends Job { mBatchSize = -1; } - return true; + // Check if user has canceled this task. We should check it again here as user cancels + // tasks in main thread, but this is running in a worker thread. calculateSize() may + // take a long time during which user can cancel this task, and we don't want to waste + // resources doing useless large chunk of work. + if (isCanceled()) { + return false; + } + + return checkSpace(); } @Override @@ -286,6 +296,44 @@ class CopyJob extends Job { return !root.isDownloads() || !doc.isDirectory(); } + /** + * Checks whether the destination folder has enough space to take all source files. + * @return true if the root has enough space or doesn't provide free space info; otherwise false + */ + boolean checkSpace() { + return checkSpace(mBatchSize); + } + + /** + * Checks whether the destination folder has enough space to take files of batchSize + * @param batchSize the total size of files + * @return true if the root has enough space or doesn't provide free space info; otherwise false + */ + final boolean checkSpace(long batchSize) { + // Default to be true because if batchSize or available space is invalid, we still let the + // copy start anyway. + boolean result = true; + if (batchSize >= 0) { + RootsCache cache = DocumentsApplication.getRootsCache(appContext); + + // Query root info here instead of using stack.root because the number there may be + // stale. + RootInfo root = cache.getRootOneshot(stack.root.authority, stack.root.rootId, true); + if (root.availableBytes >= 0) { + result = (batchSize <= root.availableBytes); + } else { + Log.w(TAG, root.toString() + " doesn't provide available bytes."); + } + } + + if (!result) { + failedFileCount += mSrcs.size(); + failedFiles.addAll(mSrcs); + } + + return result; + } + @Override boolean hasWarnings() { return !convertedFiles.isEmpty(); @@ -585,7 +633,7 @@ class CopyJob extends Job { result += calculateFileSizesRecursively(getClient(src), src.derivedUri); } catch (RemoteException e) { throw new ResourceException("Failed to obtain the client for %s.", - src.derivedUri); + src.derivedUri, e); } } else { result += src.size; @@ -603,7 +651,7 @@ class CopyJob extends Job { * * @throws ResourceException */ - private long calculateFileSizesRecursively( + long calculateFileSizesRecursively( ContentProviderClient client, Uri uri) throws ResourceException { final String authority = uri.getAuthority(); final Uri queryUri = buildChildDocumentsUri(authority, getDocumentId(uri)); diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java b/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java index 5e9d5cca014ac..beae9a80308e4 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java +++ b/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java @@ -29,8 +29,8 @@ import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.util.Log; -import com.android.documentsui.UrisSupplier; import com.android.documentsui.R; +import com.android.documentsui.UrisSupplier; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.DocumentStack; @@ -96,9 +96,35 @@ final class MoveJob extends CopyJob { return super.setUp(); } + /** + * {@inheritDoc} + * + * Only check space for moves across authorities. For now we don't know if the doc in + * {@link #mSrcs} is in the same root of destination, and if it's optimized move in the same + * root it should succeed regardless of free space, but it's for sure a failure if there is no + * enough free space if docs are moved from another authority. + */ @Override - public void start() { - super.start(); + boolean checkSpace() { + long size = 0; + for (DocumentInfo src : mSrcs) { + if (!src.authority.equals(stack.root.authority)) { + if (src.isDirectory()) { + try { + size += calculateFileSizesRecursively(getClient(src), src.derivedUri); + } catch (RemoteException|ResourceException e) { + Log.w(TAG, "Failed to obtain client for %s" + src.derivedUri + ".", e); + + // Failed to calculate size, but move may still succeed. + return true; + } + } else { + size += src.size; + } + } + } + + return checkSpace(size); } void processDocument(DocumentInfo src, DocumentInfo srcParent, DocumentInfo dest)