Home | History | Annotate | Download | only in services
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.documentsui.services;
     18 
     19 import static android.os.SystemClock.elapsedRealtime;
     20 import static android.provider.DocumentsContract.buildChildDocumentsUri;
     21 import static android.provider.DocumentsContract.buildDocumentUri;
     22 import static android.provider.DocumentsContract.getDocumentId;
     23 import static android.provider.DocumentsContract.isChildDocument;
     24 
     25 import static com.android.documentsui.OperationDialogFragment.DIALOG_TYPE_CONVERTED;
     26 import static com.android.documentsui.base.DocumentInfo.getCursorLong;
     27 import static com.android.documentsui.base.DocumentInfo.getCursorString;
     28 import static com.android.documentsui.base.Shared.DEBUG;
     29 import static com.android.documentsui.services.FileOperationService.EXTRA_DIALOG_TYPE;
     30 import static com.android.documentsui.services.FileOperationService.EXTRA_FAILED_DOCS;
     31 import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION_TYPE;
     32 import static com.android.documentsui.services.FileOperationService.MESSAGE_FINISH;
     33 import static com.android.documentsui.services.FileOperationService.MESSAGE_PROGRESS;
     34 import static com.android.documentsui.services.FileOperationService.OPERATION_COPY;
     35 
     36 import android.annotation.StringRes;
     37 import android.app.Notification;
     38 import android.app.Notification.Builder;
     39 import android.app.PendingIntent;
     40 import android.content.ContentProviderClient;
     41 import android.content.Context;
     42 import android.content.Intent;
     43 import android.content.res.AssetFileDescriptor;
     44 import android.database.ContentObserver;
     45 import android.database.Cursor;
     46 import android.net.Uri;
     47 import android.os.CancellationSignal;
     48 import android.os.Handler;
     49 import android.os.Looper;
     50 import android.os.Message;
     51 import android.os.Messenger;
     52 import android.os.ParcelFileDescriptor;
     53 import android.os.RemoteException;
     54 import android.os.storage.StorageManager;
     55 import android.provider.DocumentsContract;
     56 import android.provider.DocumentsContract.Document;
     57 import android.system.ErrnoException;
     58 import android.system.Os;
     59 import android.system.OsConstants;
     60 import android.text.format.DateUtils;
     61 import android.util.Log;
     62 import android.webkit.MimeTypeMap;
     63 
     64 import com.android.documentsui.DocumentsApplication;
     65 import com.android.documentsui.Metrics;
     66 import com.android.documentsui.R;
     67 import com.android.documentsui.base.DocumentInfo;
     68 import com.android.documentsui.base.DocumentStack;
     69 import com.android.documentsui.base.Features;
     70 import com.android.documentsui.base.RootInfo;
     71 import com.android.documentsui.clipping.UrisSupplier;
     72 import com.android.documentsui.roots.ProvidersCache;
     73 import com.android.documentsui.services.FileOperationService.OpType;
     74 
     75 import libcore.io.IoUtils;
     76 
     77 import java.io.FileDescriptor;
     78 import java.io.FileNotFoundException;
     79 import java.io.IOException;
     80 import java.io.InputStream;
     81 import java.io.SyncFailedException;
     82 import java.text.NumberFormat;
     83 import java.util.ArrayList;
     84 
     85 class CopyJob extends ResolvedResourcesJob {
     86 
     87     private static final String TAG = "CopyJob";
     88 
     89     private static final long LOADING_TIMEOUT = 60000; // 1 min
     90 
     91     final ArrayList<DocumentInfo> convertedFiles = new ArrayList<>();
     92     DocumentInfo mDstInfo;
     93 
     94     private final Handler mHandler = new Handler(Looper.getMainLooper());
     95     private final Messenger mMessenger;
     96 
     97     private long mStartTime = -1;
     98     private long mBytesRequired;
     99     private volatile long mBytesCopied;
    100 
    101     // Speed estimation.
    102     private long mBytesCopiedSample;
    103     private long mSampleTime;
    104     private long mSpeed;
    105     private long mRemainingTime;
    106 
    107     /**
    108      * @see @link {@link Job} constructor for most param descriptions.
    109      */
    110     CopyJob(Context service, Listener listener, String id, DocumentStack destination,
    111             UrisSupplier srcs, Messenger messenger, Features features) {
    112         this(service, listener, id, OPERATION_COPY, destination, srcs, messenger, features);
    113     }
    114 
    115     CopyJob(Context service, Listener listener, String id, @OpType int opType,
    116             DocumentStack destination, UrisSupplier srcs, Messenger messenger, Features features) {
    117         super(service, listener, id, opType, destination, srcs, features);
    118         mDstInfo = destination.peek();
    119         mMessenger = messenger;
    120 
    121         assert(srcs.getItemCount() > 0);
    122     }
    123 
    124     @Override
    125     Builder createProgressBuilder() {
    126         return super.createProgressBuilder(
    127                 service.getString(R.string.copy_notification_title),
    128                 R.drawable.ic_menu_copy,
    129                 service.getString(android.R.string.cancel),
    130                 R.drawable.ic_cab_cancel);
    131     }
    132 
    133     @Override
    134     public Notification getSetupNotification() {
    135         return getSetupNotification(service.getString(R.string.copy_preparing));
    136     }
    137 
    138     Notification getProgressNotification(@StringRes int msgId) {
    139         updateRemainingTimeEstimate();
    140 
    141         if (mBytesRequired >= 0) {
    142             double completed = (double) this.mBytesCopied / mBytesRequired;
    143             mProgressBuilder.setProgress(100, (int) (completed * 100), false);
    144             mProgressBuilder.setSubText(
    145                     NumberFormat.getPercentInstance().format(completed));
    146         } else {
    147             // If the total file size failed to compute on some files, then show
    148             // an indeterminate spinner. CopyJob would most likely fail on those
    149             // files while copying, but would continue with another files.
    150             // Also, if the total size is 0 bytes, show an indeterminate spinner.
    151             mProgressBuilder.setProgress(0, 0, true);
    152         }
    153 
    154         if (mRemainingTime > 0) {
    155             mProgressBuilder.setContentText(service.getString(msgId,
    156                     DateUtils.formatDuration(mRemainingTime)));
    157         } else {
    158             mProgressBuilder.setContentText(null);
    159         }
    160 
    161         return mProgressBuilder.build();
    162     }
    163 
    164     @Override
    165     public Notification getProgressNotification() {
    166         return getProgressNotification(R.string.copy_remaining);
    167     }
    168 
    169     void onBytesCopied(long numBytes) {
    170         this.mBytesCopied += numBytes;
    171     }
    172 
    173     @Override
    174     void finish() {
    175         try {
    176             mMessenger.send(Message.obtain(mHandler, MESSAGE_FINISH, 0, 0));
    177         } catch (RemoteException e) {
    178             // Ignore. Most likely the frontend was killed.
    179         }
    180         super.finish();
    181     }
    182 
    183     /**
    184      * Generates an estimate of the remaining time in the copy.
    185      */
    186     private void updateRemainingTimeEstimate() {
    187         long elapsedTime = elapsedRealtime() - mStartTime;
    188 
    189         // mBytesCopied is modified in worker thread, but this method is called in monitor thread,
    190         // so take a snapshot of mBytesCopied to make sure the updated estimate is consistent.
    191         final long bytesCopied = mBytesCopied;
    192         final long sampleDuration = Math.max(elapsedTime - mSampleTime, 1L); // avoid dividing 0
    193         final long sampleSpeed = ((bytesCopied - mBytesCopiedSample) * 1000) / sampleDuration;
    194         if (mSpeed == 0) {
    195             mSpeed = sampleSpeed;
    196         } else {
    197             mSpeed = ((3 * mSpeed) + sampleSpeed) / 4;
    198         }
    199 
    200         if (mSampleTime > 0 && mSpeed > 0) {
    201             mRemainingTime = ((mBytesRequired - bytesCopied) * 1000) / mSpeed;
    202         } else {
    203             mRemainingTime = 0;
    204         }
    205 
    206         mSampleTime = elapsedTime;
    207         mBytesCopiedSample = bytesCopied;
    208     }
    209 
    210     @Override
    211     Notification getFailureNotification() {
    212         return getFailureNotification(
    213                 R.plurals.copy_error_notification_title, R.drawable.ic_menu_copy);
    214     }
    215 
    216     @Override
    217     Notification getWarningNotification() {
    218         final Intent navigateIntent = buildNavigateIntent(INTENT_TAG_WARNING);
    219         navigateIntent.putExtra(EXTRA_DIALOG_TYPE, DIALOG_TYPE_CONVERTED);
    220         navigateIntent.putExtra(EXTRA_OPERATION_TYPE, operationType);
    221 
    222         navigateIntent.putParcelableArrayListExtra(EXTRA_FAILED_DOCS, convertedFiles);
    223 
    224         // TODO: Consider adding a dialog on tapping the notification with a list of
    225         // converted files.
    226         final Notification.Builder warningBuilder = createNotificationBuilder()
    227                 .setContentTitle(service.getResources().getString(
    228                         R.string.notification_copy_files_converted_title))
    229                 .setContentText(service.getString(
    230                         R.string.notification_touch_for_details))
    231                 .setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent,
    232                         PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
    233                 .setCategory(Notification.CATEGORY_ERROR)
    234                 .setSmallIcon(R.drawable.ic_menu_copy)
    235                 .setAutoCancel(true);
    236         return warningBuilder.build();
    237     }
    238 
    239     @Override
    240     boolean setUp() {
    241         if (!super.setUp()) {
    242             return false;
    243         }
    244 
    245         // Check if user has canceled this task.
    246         if (isCanceled()) {
    247             return false;
    248         }
    249 
    250         try {
    251             mBytesRequired = calculateBytesRequired();
    252         } catch (ResourceException e) {
    253             Log.w(TAG, "Failed to calculate total size. Copying without progress.", e);
    254             mBytesRequired = -1;
    255         }
    256 
    257         // Check if user has canceled this task. We should check it again here as user cancels
    258         // tasks in main thread, but this is running in a worker thread. calculateSize() may
    259         // take a long time during which user can cancel this task, and we don't want to waste
    260         // resources doing useless large chunk of work.
    261         if (isCanceled()) {
    262             return false;
    263         }
    264 
    265         return checkSpace();
    266     }
    267 
    268     @Override
    269     void start() {
    270         mStartTime = elapsedRealtime();
    271         DocumentInfo srcInfo;
    272         for (int i = 0; i < mResolvedDocs.size() && !isCanceled(); ++i) {
    273             srcInfo = mResolvedDocs.get(i);
    274 
    275             if (DEBUG) Log.d(TAG,
    276                     "Copying " + srcInfo.displayName + " (" + srcInfo.derivedUri + ")"
    277                     + " to " + mDstInfo.displayName + " (" + mDstInfo.derivedUri + ")");
    278 
    279             try {
    280                 // Copying recursively to itself or one of descendants is not allowed.
    281                 if (mDstInfo.equals(srcInfo) || isDescendentOf(srcInfo, mDstInfo)) {
    282                     Log.e(TAG, "Skipping recursive copy of " + srcInfo.derivedUri);
    283                     onFileFailed(srcInfo);
    284                 } else {
    285                     processDocument(srcInfo, null, mDstInfo);
    286                 }
    287             } catch (ResourceException e) {
    288                 Log.e(TAG, "Failed to copy " + srcInfo.derivedUri, e);
    289                 onFileFailed(srcInfo);
    290             }
    291         }
    292 
    293         Metrics.logFileOperation(service, operationType, mResolvedDocs, mDstInfo);
    294     }
    295 
    296     /**
    297      * Checks whether the destination folder has enough space to take all source files.
    298      * @return true if the root has enough space or doesn't provide free space info; otherwise false
    299      */
    300     boolean checkSpace() {
    301         return verifySpaceAvailable(mBytesRequired);
    302     }
    303 
    304     /**
    305      * Checks whether the destination folder has enough space to take files of batchSize
    306      * @param batchSize the total size of files
    307      * @return true if the root has enough space or doesn't provide free space info; otherwise false
    308      */
    309     final boolean verifySpaceAvailable(long batchSize) {
    310         // Default to be true because if batchSize or available space is invalid, we still let the
    311         // copy start anyway.
    312         boolean available = true;
    313         if (batchSize >= 0) {
    314             ProvidersCache cache = DocumentsApplication.getProvidersCache(appContext);
    315 
    316             RootInfo root = stack.getRoot();
    317             // Query root info here instead of using stack.root because the number there may be
    318             // stale.
    319             root = cache.getRootOneshot(root.authority, root.rootId, true);
    320             if (root.availableBytes >= 0) {
    321                 available = (batchSize <= root.availableBytes);
    322             } else {
    323                 Log.w(TAG, root.toString() + " doesn't provide available bytes.");
    324             }
    325         }
    326 
    327         if (!available) {
    328             failureCount = mResolvedDocs.size();
    329             failedDocs.addAll(mResolvedDocs);
    330         }
    331 
    332         return available;
    333     }
    334 
    335     @Override
    336     boolean hasWarnings() {
    337         return !convertedFiles.isEmpty();
    338     }
    339 
    340     /**
    341      * Logs progress on the current copy operation. Displays/Updates the progress notification.
    342      *
    343      * @param bytesCopied
    344      */
    345     private void makeCopyProgress(long bytesCopied) {
    346         final int completed =
    347             mBytesRequired >= 0 ? (int) (100.0 * this.mBytesCopied / mBytesRequired) : -1;
    348         try {
    349             mMessenger.send(Message.obtain(mHandler, MESSAGE_PROGRESS,
    350                     completed, (int) mRemainingTime));
    351         } catch (RemoteException e) {
    352             // Ignore. The frontend may be gone.
    353         }
    354         onBytesCopied(bytesCopied);
    355     }
    356 
    357     /**
    358      * Copies a the given document to the given location.
    359      *
    360      * @param src DocumentInfos for the documents to copy.
    361      * @param srcParent DocumentInfo for the parent of the document to process.
    362      * @param dstDirInfo The destination directory.
    363      * @throws ResourceException
    364      *
    365      * TODO: Stop passing srcParent, as it's not used for copy, but for move only.
    366      */
    367     void processDocument(DocumentInfo src, DocumentInfo srcParent,
    368             DocumentInfo dstDirInfo) throws ResourceException {
    369 
    370         // TODO: When optimized copy kicks in, we'll not making any progress updates.
    371         // For now. Local storage isn't using optimized copy.
    372 
    373         // When copying within the same provider, try to use optimized copying.
    374         // If not supported, then fallback to byte-by-byte copy/move.
    375         if (src.authority.equals(dstDirInfo.authority)) {
    376             if ((src.flags & Document.FLAG_SUPPORTS_COPY) != 0) {
    377                 try {
    378                     if (DocumentsContract.copyDocument(getClient(src), src.derivedUri,
    379                             dstDirInfo.derivedUri) != null) {
    380                         Metrics.logFileOperated(
    381                                 appContext, operationType, Metrics.OPMODE_PROVIDER);
    382                         return;
    383                     }
    384                 } catch (RemoteException | RuntimeException e) {
    385                     Log.e(TAG, "Provider side copy failed for: " + src.derivedUri
    386                             + " due to an exception.", e);
    387                     Metrics.logFileOperationFailure(
    388                             appContext, Metrics.SUBFILEOP_QUICK_COPY, src.derivedUri);
    389                 }
    390 
    391                 // If optimized copy fails, then fallback to byte-by-byte copy.
    392                 if (DEBUG) Log.d(TAG, "Fallback to byte-by-byte copy for: " + src.derivedUri);
    393             }
    394         }
    395 
    396         // If we couldn't do an optimized copy...we fall back to vanilla byte copy.
    397         byteCopyDocument(src, dstDirInfo);
    398     }
    399 
    400     void byteCopyDocument(DocumentInfo src, DocumentInfo dest) throws ResourceException {
    401         final String dstMimeType;
    402         final String dstDisplayName;
    403 
    404         if (DEBUG) Log.d(TAG, "Doing byte copy of document: " + src);
    405         // If the file is virtual, but can be converted to another format, then try to copy it
    406         // as such format. Also, append an extension for the target mime type (if known).
    407         if (src.isVirtual()) {
    408             String[] streamTypes = null;
    409             try {
    410                 streamTypes = getContentResolver().getStreamTypes(src.derivedUri, "*/*");
    411             } catch (RuntimeException e) {
    412                 Metrics.logFileOperationFailure(
    413                         appContext, Metrics.SUBFILEOP_OBTAIN_STREAM_TYPE, src.derivedUri);
    414                 throw new ResourceException(
    415                         "Failed to obtain streamable types for %s due to an exception.",
    416                         src.derivedUri, e);
    417             }
    418             if (streamTypes != null && streamTypes.length > 0) {
    419                 dstMimeType = streamTypes[0];
    420                 final String extension = MimeTypeMap.getSingleton().
    421                         getExtensionFromMimeType(dstMimeType);
    422                 dstDisplayName = src.displayName +
    423                         (extension != null ? "." + extension : src.displayName);
    424             } else {
    425                 Metrics.logFileOperationFailure(
    426                         appContext, Metrics.SUBFILEOP_OBTAIN_STREAM_TYPE, src.derivedUri);
    427                 throw new ResourceException("Cannot copy virtual file %s. No streamable formats "
    428                         + "available.", src.derivedUri);
    429             }
    430         } else {
    431             dstMimeType = src.mimeType;
    432             dstDisplayName = src.displayName;
    433         }
    434 
    435         // Create the target document (either a file or a directory), then copy recursively the
    436         // contents (bytes or children).
    437         Uri dstUri = null;
    438         try {
    439             dstUri = DocumentsContract.createDocument(
    440                     getClient(dest), dest.derivedUri, dstMimeType, dstDisplayName);
    441         } catch (RemoteException | RuntimeException e) {
    442             Metrics.logFileOperationFailure(
    443                     appContext, Metrics.SUBFILEOP_CREATE_DOCUMENT, dest.derivedUri);
    444             throw new ResourceException(
    445                     "Couldn't create destination document " + dstDisplayName + " in directory %s "
    446                     + "due to an exception.", dest.derivedUri, e);
    447         }
    448         if (dstUri == null) {
    449             // If this is a directory, the entire subdir will not be copied over.
    450             Metrics.logFileOperationFailure(
    451                     appContext, Metrics.SUBFILEOP_CREATE_DOCUMENT, dest.derivedUri);
    452             throw new ResourceException(
    453                     "Couldn't create destination document " + dstDisplayName + " in directory %s.",
    454                     dest.derivedUri);
    455         }
    456 
    457         DocumentInfo dstInfo = null;
    458         try {
    459             dstInfo = DocumentInfo.fromUri(getContentResolver(), dstUri);
    460         } catch (FileNotFoundException | RuntimeException e) {
    461             Metrics.logFileOperationFailure(
    462                     appContext, Metrics.SUBFILEOP_QUERY_DOCUMENT, dstUri);
    463             throw new ResourceException("Could not load DocumentInfo for newly created file %s.",
    464                     dstUri);
    465         }
    466 
    467         if (Document.MIME_TYPE_DIR.equals(src.mimeType)) {
    468             copyDirectoryHelper(src, dstInfo);
    469         } else {
    470             copyFileHelper(src, dstInfo, dest, dstMimeType);
    471         }
    472     }
    473 
    474     /**
    475      * Handles recursion into a directory and copying its contents. Note that in linux terms, this
    476      * does the equivalent of "cp src/* dst", not "cp -r src dst".
    477      *
    478      * @param srcDir Info of the directory to copy from. The routine will copy the directory's
    479      *            contents, not the directory itself.
    480      * @param destDir Info of the directory to copy to. Must be created beforehand.
    481      * @throws ResourceException
    482      */
    483     private void copyDirectoryHelper(DocumentInfo srcDir, DocumentInfo destDir)
    484             throws ResourceException {
    485         // Recurse into directories. Copy children into the new subdirectory.
    486         final String queryColumns[] = new String[] {
    487                 Document.COLUMN_DISPLAY_NAME,
    488                 Document.COLUMN_DOCUMENT_ID,
    489                 Document.COLUMN_MIME_TYPE,
    490                 Document.COLUMN_SIZE,
    491                 Document.COLUMN_FLAGS
    492         };
    493         Cursor cursor = null;
    494         boolean success = true;
    495         // Iterate over srcs in the directory; copy to the destination directory.
    496         try {
    497             try {
    498                 cursor = queryChildren(srcDir, queryColumns);
    499             } catch (RemoteException | RuntimeException e) {
    500                 Metrics.logFileOperationFailure(
    501                         appContext, Metrics.SUBFILEOP_QUERY_CHILDREN, srcDir.derivedUri);
    502                 throw new ResourceException("Failed to query children of %s due to an exception.",
    503                         srcDir.derivedUri, e);
    504             }
    505 
    506             DocumentInfo src;
    507             while (cursor.moveToNext() && !isCanceled()) {
    508                 try {
    509                     src = DocumentInfo.fromCursor(cursor, srcDir.authority);
    510                     processDocument(src, srcDir, destDir);
    511                 } catch (RuntimeException e) {
    512                     Log.e(TAG, String.format(
    513                             "Failed to recursively process a file %s due to an exception.",
    514                             srcDir.derivedUri.toString()), e);
    515                     success = false;
    516                 }
    517             }
    518         } catch (RuntimeException e) {
    519             Log.e(TAG, String.format(
    520                     "Failed to copy a file %s to %s. ",
    521                     srcDir.derivedUri.toString(), destDir.derivedUri.toString()), e);
    522             success = false;
    523         } finally {
    524             IoUtils.closeQuietly(cursor);
    525         }
    526 
    527         if (!success) {
    528             throw new RuntimeException("Some files failed to copy during a recursive "
    529                     + "directory copy.");
    530         }
    531     }
    532 
    533     /**
    534      * Handles copying a single file.
    535      *
    536      * @param src Info of the file to copy from.
    537      * @param dest Info of the *file* to copy to. Must be created beforehand.
    538      * @param destParent Info of the parent of the destination.
    539      * @param mimeType Mime type for the target. Can be different than source for virtual files.
    540      * @throws ResourceException
    541      */
    542     private void copyFileHelper(DocumentInfo src, DocumentInfo dest, DocumentInfo destParent,
    543             String mimeType) throws ResourceException {
    544         CancellationSignal canceller = new CancellationSignal();
    545         AssetFileDescriptor srcFileAsAsset = null;
    546         ParcelFileDescriptor srcFile = null;
    547         ParcelFileDescriptor dstFile = null;
    548         InputStream in = null;
    549         ParcelFileDescriptor.AutoCloseOutputStream out = null;
    550         boolean success = false;
    551 
    552         try {
    553             // If the file is virtual, but can be converted to another format, then try to copy it
    554             // as such format.
    555             if (src.isVirtual()) {
    556                 try {
    557                     srcFileAsAsset = getClient(src).openTypedAssetFileDescriptor(
    558                                 src.derivedUri, mimeType, null, canceller);
    559                 } catch (FileNotFoundException | RemoteException | RuntimeException e) {
    560                     Metrics.logFileOperationFailure(
    561                             appContext, Metrics.SUBFILEOP_OPEN_FILE, src.derivedUri);
    562                     throw new ResourceException("Failed to open a file as asset for %s due to an "
    563                             + "exception.", src.derivedUri, e);
    564                 }
    565                 srcFile = srcFileAsAsset.getParcelFileDescriptor();
    566                 try {
    567                     in = new AssetFileDescriptor.AutoCloseInputStream(srcFileAsAsset);
    568                 } catch (IOException e) {
    569                     Metrics.logFileOperationFailure(
    570                             appContext, Metrics.SUBFILEOP_OPEN_FILE, src.derivedUri);
    571                     throw new ResourceException("Failed to open a file input stream for %s due "
    572                             + "an exception.", src.derivedUri, e);
    573                 }
    574 
    575                 Metrics.logFileOperated(
    576                         appContext, operationType, Metrics.OPMODE_CONVERTED);
    577             } else {
    578                 try {
    579                     srcFile = getClient(src).openFile(src.derivedUri, "r", canceller);
    580                 } catch (FileNotFoundException | RemoteException | RuntimeException e) {
    581                     Metrics.logFileOperationFailure(
    582                             appContext, Metrics.SUBFILEOP_OPEN_FILE, src.derivedUri);
    583                     throw new ResourceException(
    584                             "Failed to open a file for %s due to an exception.", src.derivedUri, e);
    585                 }
    586                 in = new ParcelFileDescriptor.AutoCloseInputStream(srcFile);
    587 
    588                 Metrics.logFileOperated(
    589                         appContext, operationType, Metrics.OPMODE_CONVENTIONAL);
    590             }
    591 
    592             try {
    593                 dstFile = getClient(dest).openFile(dest.derivedUri, "w", canceller);
    594             } catch (FileNotFoundException | RemoteException | RuntimeException e) {
    595                 Metrics.logFileOperationFailure(
    596                         appContext, Metrics.SUBFILEOP_OPEN_FILE, dest.derivedUri);
    597                 throw new ResourceException("Failed to open the destination file %s for writing "
    598                         + "due to an exception.", dest.derivedUri, e);
    599             }
    600             out = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);
    601 
    602             byte[] buffer = new byte[32 * 1024];
    603             int len;
    604             boolean reading = true;
    605             try {
    606                 // If we know the source size, and the destination supports disk
    607                 // space allocation, then allocate the space we'll need. This
    608                 // uses fallocate() under the hood to optimize on-disk layout
    609                 // and prevent us from running out of space during large copies.
    610                 final StorageManager sm = service.getSystemService(StorageManager.class);
    611                 final long srcSize = srcFile.getStatSize();
    612                 final FileDescriptor dstFd = dstFile.getFileDescriptor();
    613                 if (srcSize > 0 && sm.isAllocationSupported(dstFd)) {
    614                     sm.allocateBytes(dstFd, srcSize);
    615                 }
    616 
    617                 while ((len = in.read(buffer)) != -1) {
    618                     if (isCanceled()) {
    619                         if (DEBUG) Log.d(TAG, "Canceled copy mid-copy of: " + src.derivedUri);
    620                         return;
    621                     }
    622                     reading = false;
    623                     out.write(buffer, 0, len);
    624                     makeCopyProgress(len);
    625                     reading = true;
    626                 }
    627 
    628                 reading = false;
    629                 // Need to invoke Os#fsync to ensure the file is written to the storage device.
    630                 try {
    631                     Os.fsync(dstFile.getFileDescriptor());
    632                 } catch (ErrnoException error) {
    633                     // fsync will fail with fd of pipes and return EROFS or EINVAL.
    634                     if (error.errno != OsConstants.EROFS && error.errno != OsConstants.EINVAL) {
    635                         throw new SyncFailedException(
    636                                 "Failed to sync bytes after copying a file.");
    637                     }
    638                 }
    639 
    640                 // Need to invoke IoUtils.close explicitly to avoid from ignoring errors at flush.
    641                 IoUtils.close(dstFile.getFileDescriptor());
    642                 srcFile.checkError();
    643             } catch (IOException e) {
    644                 Metrics.logFileOperationFailure(
    645                         appContext,
    646                         reading ? Metrics.SUBFILEOP_READ_FILE : Metrics.SUBFILEOP_WRITE_FILE,
    647                         reading ? src.derivedUri: dest.derivedUri);
    648                 throw new ResourceException(
    649                         "Failed to copy bytes from %s to %s due to an IO exception.",
    650                         src.derivedUri, dest.derivedUri, e);
    651             }
    652 
    653             if (src.isVirtual()) {
    654                convertedFiles.add(src);
    655             }
    656 
    657             success = true;
    658         } finally {
    659             if (!success) {
    660                 if (dstFile != null) {
    661                     try {
    662                         dstFile.closeWithError("Error copying bytes.");
    663                     } catch (IOException closeError) {
    664                         Log.w(TAG, "Error closing destination.", closeError);
    665                     }
    666                 }
    667 
    668                 if (DEBUG) Log.d(TAG, "Cleaning up failed operation leftovers.");
    669                 canceller.cancel();
    670                 try {
    671                     deleteDocument(dest, destParent);
    672                 } catch (ResourceException e) {
    673                     Log.w(TAG, "Failed to cleanup after copy error: " + src.derivedUri, e);
    674                 }
    675             }
    676 
    677             // This also ensures the file descriptors are closed.
    678             IoUtils.closeQuietly(in);
    679             IoUtils.closeQuietly(out);
    680         }
    681     }
    682 
    683     /**
    684      * Calculates the cumulative size of all the documents in the list. Directories are recursed
    685      * into and totaled up.
    686      *
    687      * @return Size in bytes.
    688      * @throws ResourceException
    689      */
    690     private long calculateBytesRequired() throws ResourceException {
    691         long result = 0;
    692 
    693         for (DocumentInfo src : mResolvedDocs) {
    694             if (src.isDirectory()) {
    695                 // Directories need to be recursed into.
    696                 try {
    697                     result += calculateFileSizesRecursively(getClient(src), src.derivedUri);
    698                 } catch (RemoteException e) {
    699                     throw new ResourceException("Failed to obtain the client for %s.",
    700                             src.derivedUri, e);
    701                 }
    702             } else {
    703                 result += src.size;
    704             }
    705 
    706             if (isCanceled()) {
    707                 return result;
    708             }
    709         }
    710         return result;
    711     }
    712 
    713     /**
    714      * Calculates (recursively) the cumulative size of all the files under the given directory.
    715      *
    716      * @throws ResourceException
    717      */
    718     long calculateFileSizesRecursively(
    719             ContentProviderClient client, Uri uri) throws ResourceException {
    720         final String authority = uri.getAuthority();
    721         final String queryColumns[] = new String[] {
    722                 Document.COLUMN_DOCUMENT_ID,
    723                 Document.COLUMN_MIME_TYPE,
    724                 Document.COLUMN_SIZE
    725         };
    726 
    727         long result = 0;
    728         Cursor cursor = null;
    729         try {
    730             cursor = queryChildren(client, uri, queryColumns);
    731             while (cursor.moveToNext() && !isCanceled()) {
    732                 if (Document.MIME_TYPE_DIR.equals(
    733                         getCursorString(cursor, Document.COLUMN_MIME_TYPE))) {
    734                     // Recurse into directories.
    735                     final Uri dirUri = buildDocumentUri(authority,
    736                             getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
    737                     result += calculateFileSizesRecursively(client, dirUri);
    738                 } else {
    739                     // This may return -1 if the size isn't defined. Ignore those cases.
    740                     long size = getCursorLong(cursor, Document.COLUMN_SIZE);
    741                     result += size > 0 ? size : 0;
    742                 }
    743             }
    744         } catch (RemoteException | RuntimeException e) {
    745             throw new ResourceException(
    746                     "Failed to calculate size for %s due to an exception.", uri, e);
    747         } finally {
    748             IoUtils.closeQuietly(cursor);
    749         }
    750 
    751         return result;
    752     }
    753 
    754     /**
    755      * Queries children documents.
    756      *
    757      * SAF allows {@link DocumentsContract#EXTRA_LOADING} in {@link Cursor#getExtras()} to indicate
    758      * there are more data to be loaded. Wait until {@link DocumentsContract#EXTRA_LOADING} is
    759      * false and then return the cursor.
    760      *
    761      * @param srcDir the directory whose children are being loading
    762      * @param queryColumns columns of metadata to load
    763      * @return cursor of all children documents
    764      * @throws RemoteException when the remote throws or waiting for update times out
    765      */
    766     private Cursor queryChildren(DocumentInfo srcDir, String[] queryColumns)
    767             throws RemoteException {
    768         try (final ContentProviderClient client = getClient(srcDir)) {
    769             return queryChildren(client, srcDir.derivedUri, queryColumns);
    770         }
    771     }
    772 
    773     /**
    774      * Queries children documents.
    775      *
    776      * SAF allows {@link DocumentsContract#EXTRA_LOADING} in {@link Cursor#getExtras()} to indicate
    777      * there are more data to be loaded. Wait until {@link DocumentsContract#EXTRA_LOADING} is
    778      * false and then return the cursor.
    779      *
    780      * @param client the {@link ContentProviderClient} to use to query children
    781      * @param dirDocUri the document Uri of the directory whose children are being loaded
    782      * @param queryColumns columns of metadata to load
    783      * @return cursor of all children documents
    784      * @throws RemoteException when the remote throws or waiting for update times out
    785      */
    786     private Cursor queryChildren(ContentProviderClient client, Uri dirDocUri, String[] queryColumns)
    787             throws RemoteException {
    788         // TODO (b/34459983): Optimize this performance by processing partial result first while provider is loading
    789         // more data. Note we need to skip size calculation to achieve it.
    790         final Uri queryUri = buildChildDocumentsUri(dirDocUri.getAuthority(), getDocumentId(dirDocUri));
    791         Cursor cursor = client.query(
    792                 queryUri, queryColumns, (String) null, null, null);
    793         while (cursor.getExtras().getBoolean(DocumentsContract.EXTRA_LOADING)) {
    794             cursor.registerContentObserver(new DirectoryChildrenObserver(queryUri));
    795             try {
    796                 long start = System.currentTimeMillis();
    797                 synchronized (queryUri) {
    798                     queryUri.wait(LOADING_TIMEOUT);
    799                 }
    800                 if (System.currentTimeMillis() - start > LOADING_TIMEOUT) {
    801                     // Timed out
    802                     throw new RemoteException("Timed out waiting on update for " + queryUri);
    803                 }
    804             } catch (InterruptedException e) {
    805                 // Should never happen
    806                 throw new RuntimeException(e);
    807             }
    808 
    809             // Make another query
    810             cursor = client.query(
    811                     queryUri, queryColumns, (String) null, null, null);
    812         }
    813 
    814         return cursor;
    815     }
    816 
    817     /**
    818      * Returns true if {@code doc} is a descendant of {@code parentDoc}.
    819      * @throws ResourceException
    820      */
    821     boolean isDescendentOf(DocumentInfo doc, DocumentInfo parent)
    822             throws ResourceException {
    823         if (parent.isDirectory() && doc.authority.equals(parent.authority)) {
    824             try {
    825                 return isChildDocument(getClient(doc), doc.derivedUri, parent.derivedUri);
    826             } catch (RemoteException | RuntimeException e) {
    827                 throw new ResourceException(
    828                         "Failed to check if %s is a child of %s due to an exception.",
    829                         doc.derivedUri, parent.derivedUri, e);
    830             }
    831         }
    832         return false;
    833     }
    834 
    835     @Override
    836     public String toString() {
    837         return new StringBuilder()
    838                 .append("CopyJob")
    839                 .append("{")
    840                 .append("id=" + id)
    841                 .append(", uris=" + mResourceUris)
    842                 .append(", docs=" + mResolvedDocs)
    843                 .append(", destination=" + stack)
    844                 .append("}")
    845                 .toString();
    846     }
    847 
    848     private static class DirectoryChildrenObserver extends ContentObserver {
    849 
    850         private final Object mNotifier;
    851 
    852         private DirectoryChildrenObserver(Object notifier) {
    853             super(new Handler(Looper.getMainLooper()));
    854             assert(notifier != null);
    855             mNotifier = notifier;
    856         }
    857 
    858         @Override
    859         public void onChange(boolean selfChange, Uri uri) {
    860             synchronized (mNotifier) {
    861                 mNotifier.notify();
    862             }
    863         }
    864     }
    865 }
    866