Add free space precondition check for copy job.

Bug: 24948755
Change-Id: I210395bdf339d630604e90e867ffddbbd3cf4bea
diff --git a/src/com/android/documentsui/RootsCache.java b/src/com/android/documentsui/RootsCache.java
index 88eeb49..117bb01 100644
--- a/src/com/android/documentsui/RootsCache.java
+++ b/src/com/android/documentsui/RootsCache.java
@@ -36,6 +36,7 @@
 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 @@
      * 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/src/com/android/documentsui/services/CopyJob.java b/src/com/android/documentsui/services/CopyJob.java
index 390656c..54ccc2a 100644
--- a/src/com/android/documentsui/services/CopyJob.java
+++ b/src/com/android/documentsui/services/CopyJob.java
@@ -51,9 +51,11 @@
 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 @@
 
     @Override
     boolean setUp() {
-
         try {
             buildDocumentList();
         } catch (ResourceException e) {
@@ -218,6 +219,7 @@
             return false;
         }
 
+        // Check if user has canceled this task.
         if (isCanceled()) {
             return false;
         }
@@ -229,7 +231,15 @@
             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 @@
         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 @@
                     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 @@
      *
      * @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/src/com/android/documentsui/services/MoveJob.java b/src/com/android/documentsui/services/MoveJob.java
index 5e9d5cc..beae9a8 100644
--- a/src/com/android/documentsui/services/MoveJob.java
+++ b/src/com/android/documentsui/services/MoveJob.java
@@ -29,8 +29,8 @@
 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 @@
         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)