blob: dad86977c7a2ebc99aad6053486831f6d46fe606 [file] [log] [blame]
Steve McKay14e827a2016-01-06 18:32:13 -08001/*
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
17package com.android.documentsui.services;
18
19import static android.os.SystemClock.elapsedRealtime;
Steve McKayecbf3c52016-01-13 17:17:39 -080020import static android.provider.DocumentsContract.buildChildDocumentsUri;
21import static android.provider.DocumentsContract.buildDocumentUri;
22import static android.provider.DocumentsContract.getDocumentId;
23import static android.provider.DocumentsContract.isChildDocument;
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +090024import static com.android.documentsui.OperationDialogFragment.DIALOG_TYPE_CONVERTED;
Steve McKay14e827a2016-01-06 18:32:13 -080025import static com.android.documentsui.Shared.DEBUG;
26import static com.android.documentsui.model.DocumentInfo.getCursorLong;
27import static com.android.documentsui.model.DocumentInfo.getCursorString;
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +090028import static com.android.documentsui.services.FileOperationService.EXTRA_DIALOG_TYPE;
29import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION;
30import static com.android.documentsui.services.FileOperationService.EXTRA_SRC_LIST;
Steve McKay14e827a2016-01-06 18:32:13 -080031import static com.android.documentsui.services.FileOperationService.OPERATION_COPY;
32import static com.google.common.base.Preconditions.checkArgument;
33
34import android.annotation.StringRes;
35import android.app.Notification;
36import android.app.Notification.Builder;
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +090037import android.app.PendingIntent;
Steve McKay14e827a2016-01-06 18:32:13 -080038import android.content.ContentProviderClient;
39import android.content.Context;
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +090040import android.content.Intent;
Steve McKay14e827a2016-01-06 18:32:13 -080041import android.content.res.AssetFileDescriptor;
42import android.database.Cursor;
43import android.net.Uri;
44import android.os.CancellationSignal;
45import android.os.ParcelFileDescriptor;
46import android.os.RemoteException;
47import android.provider.DocumentsContract;
48import android.provider.DocumentsContract.Document;
49import android.text.format.DateUtils;
50import android.util.Log;
51import android.webkit.MimeTypeMap;
52
Ben Kwad5b2af12016-01-28 16:39:57 -080053import com.android.documentsui.Metrics;
Steve McKay14e827a2016-01-06 18:32:13 -080054import com.android.documentsui.R;
55import com.android.documentsui.model.DocumentInfo;
56import com.android.documentsui.model.DocumentStack;
Steve McKayecbf3c52016-01-13 17:17:39 -080057import com.android.documentsui.services.FileOperationService.OpType;
Steve McKay14e827a2016-01-06 18:32:13 -080058
59import libcore.io.IoUtils;
60
61import java.io.FileNotFoundException;
62import java.io.IOException;
63import java.io.InputStream;
64import java.io.OutputStream;
65import java.text.NumberFormat;
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +090066import java.util.ArrayList;
Steve McKay14e827a2016-01-06 18:32:13 -080067import java.util.List;
68
69class CopyJob extends Job {
70 private static final String TAG = "CopyJob";
71 private static final int PROGRESS_INTERVAL_MILLIS = 1000;
Steve McKay35645432016-01-20 15:09:35 -080072 final List<DocumentInfo> mSrcs;
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +090073 final ArrayList<DocumentInfo> convertedFiles = new ArrayList<>();
Steve McKay14e827a2016-01-06 18:32:13 -080074
75 private long mStartTime = -1;
76 private long mBatchSize;
77 private long mBytesCopied;
78 private long mLastNotificationTime;
79 // Speed estimation
80 private long mBytesCopiedSample;
81 private long mSampleTime;
82 private long mSpeed;
83 private long mRemainingTime;
84
85 /**
86 * Copies files to a destination identified by {@code destination}.
87 * @see @link {@link Job} constructor for most param descriptions.
88 *
89 * @param srcs List of files to be copied.
90 */
Steve McKayecbf3c52016-01-13 17:17:39 -080091 CopyJob(Context service, Context appContext, Listener listener,
Steve McKay35645432016-01-20 15:09:35 -080092 String id, DocumentStack stack, List<DocumentInfo> srcs) {
93 super(service, appContext, listener, OPERATION_COPY, id, stack);
Steve McKayecbf3c52016-01-13 17:17:39 -080094
95 checkArgument(!srcs.isEmpty());
Steve McKay35645432016-01-20 15:09:35 -080096 this.mSrcs = srcs;
Steve McKayecbf3c52016-01-13 17:17:39 -080097 }
98
99 /**
100 * @see @link {@link Job} constructor for most param descriptions.
101 *
102 * @param srcs List of files to be copied.
103 */
104 CopyJob(Context service, Context appContext, Listener listener,
105 @OpType int opType, String id, DocumentStack destination, List<DocumentInfo> srcs) {
106 super(service, appContext, listener, opType, id, destination);
Steve McKay14e827a2016-01-06 18:32:13 -0800107
108 checkArgument(!srcs.isEmpty());
Steve McKay35645432016-01-20 15:09:35 -0800109 this.mSrcs = srcs;
Steve McKay14e827a2016-01-06 18:32:13 -0800110 }
111
112 @Override
113 Builder createProgressBuilder() {
114 return super.createProgressBuilder(
Steve McKayecbf3c52016-01-13 17:17:39 -0800115 service.getString(R.string.copy_notification_title),
Steve McKay14e827a2016-01-06 18:32:13 -0800116 R.drawable.ic_menu_copy,
Steve McKayecbf3c52016-01-13 17:17:39 -0800117 service.getString(android.R.string.cancel),
Steve McKay14e827a2016-01-06 18:32:13 -0800118 R.drawable.ic_cab_cancel);
119 }
120
121 @Override
122 public Notification getSetupNotification() {
Steve McKayecbf3c52016-01-13 17:17:39 -0800123 return getSetupNotification(service.getString(R.string.copy_preparing));
Steve McKay14e827a2016-01-06 18:32:13 -0800124 }
125
126 public boolean shouldUpdateProgress() {
127 // Wait a while between updates :)
128 return elapsedRealtime() - mLastNotificationTime > PROGRESS_INTERVAL_MILLIS;
129 }
130
131 Notification getProgressNotification(@StringRes int msgId) {
132 double completed = (double) this.mBytesCopied / mBatchSize;
133 mProgressBuilder.setProgress(100, (int) (completed * 100), false);
134 mProgressBuilder.setContentInfo(
135 NumberFormat.getPercentInstance().format(completed));
136 if (mRemainingTime > 0) {
Steve McKayecbf3c52016-01-13 17:17:39 -0800137 mProgressBuilder.setContentText(service.getString(msgId,
Steve McKay14e827a2016-01-06 18:32:13 -0800138 DateUtils.formatDuration(mRemainingTime)));
139 } else {
140 mProgressBuilder.setContentText(null);
141 }
142
143 // Remember when we last returned progress so we can provide an answer
144 // in shouldUpdateProgress.
145 mLastNotificationTime = elapsedRealtime();
146 return mProgressBuilder.build();
147 }
148
149 public Notification getProgressNotification() {
150 return getProgressNotification(R.string.copy_remaining);
151 }
152
153 void onBytesCopied(long numBytes) {
154 this.mBytesCopied += numBytes;
155 }
156
157 /**
158 * Generates an estimate of the remaining time in the copy.
159 */
160 void updateRemainingTimeEstimate() {
161 long elapsedTime = elapsedRealtime() - mStartTime;
162
163 final long sampleDuration = elapsedTime - mSampleTime;
164 final long sampleSpeed = ((mBytesCopied - mBytesCopiedSample) * 1000) / sampleDuration;
165 if (mSpeed == 0) {
166 mSpeed = sampleSpeed;
167 } else {
168 mSpeed = ((3 * mSpeed) + sampleSpeed) / 4;
169 }
170
171 if (mSampleTime > 0 && mSpeed > 0) {
172 mRemainingTime = ((mBatchSize - mBytesCopied) * 1000) / mSpeed;
173 } else {
174 mRemainingTime = 0;
175 }
176
177 mSampleTime = elapsedTime;
178 mBytesCopiedSample = mBytesCopied;
179 }
180
181 @Override
182 Notification getFailureNotification() {
183 return getFailureNotification(
184 R.plurals.copy_error_notification_title, R.drawable.ic_menu_copy);
185 }
186
187 @Override
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +0900188 Notification getWarningNotification() {
Tomasz Mikolajewskicd270152016-02-01 12:01:14 +0900189 final Intent navigateIntent = buildNavigateIntent(INTENT_TAG_WARNING);
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +0900190 navigateIntent.putExtra(EXTRA_DIALOG_TYPE, DIALOG_TYPE_CONVERTED);
191 navigateIntent.putExtra(EXTRA_OPERATION, operationType);
192
193 navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, convertedFiles);
194
195 // TODO: Consider adding a dialog on tapping the notification with a list of
196 // converted files.
197 final Notification.Builder warningBuilder = new Notification.Builder(service)
198 .setContentTitle(service.getResources().getString(
199 R.string.notification_copy_files_converted_title))
200 .setContentText(service.getString(
201 R.string.notification_touch_for_details))
202 .setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent,
203 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
204 .setCategory(Notification.CATEGORY_ERROR)
205 .setSmallIcon(R.drawable.ic_menu_copy)
206 .setAutoCancel(true);
207 return warningBuilder.build();
208 }
209
210 @Override
Steve McKayecbf3c52016-01-13 17:17:39 -0800211 void start() throws RemoteException {
Steve McKay14e827a2016-01-06 18:32:13 -0800212 mStartTime = elapsedRealtime();
213
Steve McKay14e827a2016-01-06 18:32:13 -0800214 // client
Steve McKay35645432016-01-20 15:09:35 -0800215 mBatchSize = calculateSize(mSrcs);
Steve McKay14e827a2016-01-06 18:32:13 -0800216
217 DocumentInfo srcInfo;
Ben Kwad5b2af12016-01-28 16:39:57 -0800218 DocumentInfo dstInfo = stack.peek();
Steve McKay35645432016-01-20 15:09:35 -0800219 for (int i = 0; i < mSrcs.size() && !isCanceled(); ++i) {
220 srcInfo = mSrcs.get(i);
Steve McKay14e827a2016-01-06 18:32:13 -0800221
222 // Guard unsupported recursive operation.
223 if (dstInfo.equals(srcInfo) || isDescendentOf(srcInfo, dstInfo)) {
Steve McKayecbf3c52016-01-13 17:17:39 -0800224 onFileFailed(srcInfo,
225 "Skipping recursive operation on directory " + dstInfo.derivedUri + ".");
Steve McKay14e827a2016-01-06 18:32:13 -0800226 continue;
227 }
228
229 if (DEBUG) Log.d(TAG,
Steve McKayecbf3c52016-01-13 17:17:39 -0800230 "Copying " + srcInfo.displayName + " (" + srcInfo.derivedUri + ")"
231 + " to " + dstInfo.displayName + " (" + dstInfo.derivedUri + ")");
Steve McKay14e827a2016-01-06 18:32:13 -0800232
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900233 processDocument(srcInfo, null, dstInfo);
Steve McKay14e827a2016-01-06 18:32:13 -0800234 }
Ben Kwad5b2af12016-01-28 16:39:57 -0800235 Metrics.logFileOperation(service, operationType, mSrcs, dstInfo);
Steve McKay14e827a2016-01-06 18:32:13 -0800236 }
237
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +0900238 @Override
239 boolean hasWarnings() {
240 return !convertedFiles.isEmpty();
241 }
242
Steve McKay14e827a2016-01-06 18:32:13 -0800243 /**
244 * Logs progress on the current copy operation. Displays/Updates the progress notification.
245 *
246 * @param bytesCopied
247 */
248 private void makeCopyProgress(long bytesCopied) {
249 onBytesCopied(bytesCopied);
250 if (shouldUpdateProgress()) {
251 updateRemainingTimeEstimate();
252 listener.onProgress(this);
253 }
254 }
255
256 /**
257 * Copies a the given document to the given location.
258 *
Steve McKay35645432016-01-20 15:09:35 -0800259 * @param src DocumentInfos for the documents to copy.
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900260 * @param srcParent DocumentInfo for the parent of the document to process.
Steve McKay14e827a2016-01-06 18:32:13 -0800261 * @param dstDirInfo The destination directory.
Steve McKay14e827a2016-01-06 18:32:13 -0800262 * @return True on success, false on failure.
263 * @throws RemoteException
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900264 *
265 * TODO: Stop passing srcParent, as it's not used for copy, but for move only.
Steve McKay14e827a2016-01-06 18:32:13 -0800266 */
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900267 boolean processDocument(DocumentInfo src, DocumentInfo srcParent,
268 DocumentInfo dstDirInfo) throws RemoteException {
Steve McKay14e827a2016-01-06 18:32:13 -0800269
270 // TODO: When optimized copy kicks in, we'll not making any progress updates.
271 // For now. Local storage isn't using optimized copy.
272
Tomasz Mikolajewski67048082016-01-21 10:00:33 +0900273 // When copying within the same provider, try to use optimized copying.
Steve McKay14e827a2016-01-06 18:32:13 -0800274 // If not supported, then fallback to byte-by-byte copy/move.
Steve McKay35645432016-01-20 15:09:35 -0800275 if (src.authority.equals(dstDirInfo.authority)) {
276 if ((src.flags & Document.FLAG_SUPPORTS_COPY) != 0) {
277 if (DocumentsContract.copyDocument(getClient(src), src.derivedUri,
Steve McKay14e827a2016-01-06 18:32:13 -0800278 dstDirInfo.derivedUri) == null) {
Steve McKay35645432016-01-20 15:09:35 -0800279 onFileFailed(src,
280 "Provider side copy failed for documents: " + src.derivedUri + ".");
Tomasz Mikolajewski67048082016-01-21 10:00:33 +0900281 return false;
Steve McKay14e827a2016-01-06 18:32:13 -0800282 }
Tomasz Mikolajewski67048082016-01-21 10:00:33 +0900283 return true;
Steve McKay14e827a2016-01-06 18:32:13 -0800284 }
285 }
286
287 // If we couldn't do an optimized copy...we fall back to vanilla byte copy.
Steve McKay35645432016-01-20 15:09:35 -0800288 return byteCopyDocument(src, dstDirInfo);
Steve McKay14e827a2016-01-06 18:32:13 -0800289 }
290
Steve McKay35645432016-01-20 15:09:35 -0800291 boolean byteCopyDocument(DocumentInfo src, DocumentInfo dest)
Steve McKay14e827a2016-01-06 18:32:13 -0800292 throws RemoteException {
293 final String dstMimeType;
294 final String dstDisplayName;
295
Steve McKay35645432016-01-20 15:09:35 -0800296 if (DEBUG) Log.d(TAG, "Doing byte copy of document: " + src);
Steve McKay14e827a2016-01-06 18:32:13 -0800297 // If the file is virtual, but can be converted to another format, then try to copy it
298 // as such format. Also, append an extension for the target mime type (if known).
Steve McKay35645432016-01-20 15:09:35 -0800299 if (src.isVirtualDocument()) {
Steve McKay14e827a2016-01-06 18:32:13 -0800300 final String[] streamTypes = getContentResolver().getStreamTypes(
Steve McKay35645432016-01-20 15:09:35 -0800301 src.derivedUri, "*/*");
Steve McKay14e827a2016-01-06 18:32:13 -0800302 if (streamTypes != null && streamTypes.length > 0) {
303 dstMimeType = streamTypes[0];
304 final String extension = MimeTypeMap.getSingleton().
305 getExtensionFromMimeType(dstMimeType);
Steve McKay35645432016-01-20 15:09:35 -0800306 dstDisplayName = src.displayName +
307 (extension != null ? "." + extension : src.displayName);
Steve McKay14e827a2016-01-06 18:32:13 -0800308 } else {
Steve McKay35645432016-01-20 15:09:35 -0800309 onFileFailed(src, "Cannot copy virtual file. No streamable formats available.");
Steve McKay14e827a2016-01-06 18:32:13 -0800310 return false;
311 }
312 } else {
Steve McKay35645432016-01-20 15:09:35 -0800313 dstMimeType = src.mimeType;
314 dstDisplayName = src.displayName;
Steve McKay14e827a2016-01-06 18:32:13 -0800315 }
316
317 // Create the target document (either a file or a directory), then copy recursively the
318 // contents (bytes or children).
Steve McKay35645432016-01-20 15:09:35 -0800319 final Uri dstUri = DocumentsContract.createDocument(
320 getClient(dest), dest.derivedUri, dstMimeType, dstDisplayName);
Steve McKay14e827a2016-01-06 18:32:13 -0800321 if (dstUri == null) {
322 // If this is a directory, the entire subdir will not be copied over.
Steve McKay35645432016-01-20 15:09:35 -0800323 onFileFailed(src,
Steve McKayecbf3c52016-01-13 17:17:39 -0800324 "Couldn't create destination document " + dstDisplayName
Steve McKay35645432016-01-20 15:09:35 -0800325 + " in directory " + dest.displayName + ".");
Steve McKay14e827a2016-01-06 18:32:13 -0800326 return false;
327 }
328
329 DocumentInfo dstInfo = null;
330 try {
331 dstInfo = DocumentInfo.fromUri(getContentResolver(), dstUri);
332 } catch (FileNotFoundException e) {
Steve McKay35645432016-01-20 15:09:35 -0800333 onFileFailed(src,
Steve McKayecbf3c52016-01-13 17:17:39 -0800334 "Could not load DocumentInfo for newly created file: " + dstUri + ".");
Steve McKay14e827a2016-01-06 18:32:13 -0800335 return false;
336 }
337
338 final boolean success;
Steve McKay35645432016-01-20 15:09:35 -0800339 if (Document.MIME_TYPE_DIR.equals(src.mimeType)) {
340 success = copyDirectoryHelper(src, dstInfo);
Steve McKay14e827a2016-01-06 18:32:13 -0800341 } else {
Steve McKay35645432016-01-20 15:09:35 -0800342 success = copyFileHelper(src, dstInfo, dstMimeType);
Steve McKay14e827a2016-01-06 18:32:13 -0800343 }
344
345 return success;
346 }
347
348 /**
349 * Handles recursion into a directory and copying its contents. Note that in linux terms, this
350 * does the equivalent of "cp src/* dst", not "cp -r src dst".
351 *
Steve McKay35645432016-01-20 15:09:35 -0800352 * @param srcDir Info of the directory to copy from. The routine will copy the directory's
Steve McKay14e827a2016-01-06 18:32:13 -0800353 * contents, not the directory itself.
Steve McKay35645432016-01-20 15:09:35 -0800354 * @param destDir Info of the directory to copy to. Must be created beforehand.
Steve McKay14e827a2016-01-06 18:32:13 -0800355 * @return True on success, false if some of the children failed to copy.
356 * @throws RemoteException
357 */
Steve McKay35645432016-01-20 15:09:35 -0800358 private boolean copyDirectoryHelper(DocumentInfo srcDir, DocumentInfo destDir)
Steve McKay14e827a2016-01-06 18:32:13 -0800359 throws RemoteException {
360 // Recurse into directories. Copy children into the new subdirectory.
361 final String queryColumns[] = new String[] {
362 Document.COLUMN_DISPLAY_NAME,
363 Document.COLUMN_DOCUMENT_ID,
364 Document.COLUMN_MIME_TYPE,
365 Document.COLUMN_SIZE,
366 Document.COLUMN_FLAGS
367 };
368 Cursor cursor = null;
369 boolean success = true;
370 try {
371 // Iterate over srcs in the directory; copy to the destination directory.
Steve McKay35645432016-01-20 15:09:35 -0800372 final Uri queryUri = buildChildDocumentsUri(srcDir.authority, srcDir.documentId);
373 cursor = getClient(srcDir).query(queryUri, queryColumns, null, null, null);
Steve McKayecbf3c52016-01-13 17:17:39 -0800374 while (cursor.moveToNext() && !isCanceled()) {
Steve McKay35645432016-01-20 15:09:35 -0800375 DocumentInfo src = DocumentInfo.fromCursor(cursor, srcDir.authority);
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900376 success &= processDocument(src, srcDir, destDir);
Steve McKay14e827a2016-01-06 18:32:13 -0800377 }
378 } finally {
379 IoUtils.closeQuietly(cursor);
380 }
381
382 return success;
383 }
384
385 /**
386 * Handles copying a single file.
387 *
388 * @param srcUriInfo Info of the file to copy from.
389 * @param dstUriInfo Info of the *file* to copy to. Must be created beforehand.
390 * @param mimeType Mime type for the target. Can be different than source for virtual files.
391 * @return True on success, false on error.
392 * @throws RemoteException
393 */
Steve McKay35645432016-01-20 15:09:35 -0800394 private boolean copyFileHelper(DocumentInfo src, DocumentInfo dest, String mimeType)
Steve McKay14e827a2016-01-06 18:32:13 -0800395 throws RemoteException {
Steve McKay14e827a2016-01-06 18:32:13 -0800396 CancellationSignal canceller = new CancellationSignal();
397 ParcelFileDescriptor srcFile = null;
398 ParcelFileDescriptor dstFile = null;
Steve McKay35645432016-01-20 15:09:35 -0800399 InputStream in = null;
400 OutputStream out = null;
Steve McKay14e827a2016-01-06 18:32:13 -0800401
402 boolean success = true;
403 try {
404 // If the file is virtual, but can be converted to another format, then try to copy it
405 // as such format.
Steve McKay35645432016-01-20 15:09:35 -0800406 if (src.isVirtualDocument()) {
Steve McKay14e827a2016-01-06 18:32:13 -0800407 final AssetFileDescriptor srcFileAsAsset =
Steve McKay35645432016-01-20 15:09:35 -0800408 getClient(src).openTypedAssetFileDescriptor(
409 src.derivedUri, mimeType, null, canceller);
Steve McKay14e827a2016-01-06 18:32:13 -0800410 srcFile = srcFileAsAsset.getParcelFileDescriptor();
Steve McKay35645432016-01-20 15:09:35 -0800411 in = new AssetFileDescriptor.AutoCloseInputStream(srcFileAsAsset);
Steve McKay14e827a2016-01-06 18:32:13 -0800412 } else {
Steve McKay35645432016-01-20 15:09:35 -0800413 srcFile = getClient(src).openFile(src.derivedUri, "r", canceller);
414 in = new ParcelFileDescriptor.AutoCloseInputStream(srcFile);
Steve McKay14e827a2016-01-06 18:32:13 -0800415 }
416
Steve McKay35645432016-01-20 15:09:35 -0800417 dstFile = getClient(dest).openFile(dest.derivedUri, "w", canceller);
418 out = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);
Steve McKay14e827a2016-01-06 18:32:13 -0800419
Steve McKayecbf3c52016-01-13 17:17:39 -0800420 byte[] buffer = new byte[32 * 1024];
Steve McKay14e827a2016-01-06 18:32:13 -0800421 int len;
Steve McKay35645432016-01-20 15:09:35 -0800422 while ((len = in.read(buffer)) != -1) {
Steve McKay14e827a2016-01-06 18:32:13 -0800423 if (isCanceled()) {
424 if (DEBUG) Log.d(TAG, "Canceled copy mid-copy. Id:" + id);
425 success = false;
426 break;
427 }
Steve McKay35645432016-01-20 15:09:35 -0800428 out.write(buffer, 0, len);
Steve McKay14e827a2016-01-06 18:32:13 -0800429 makeCopyProgress(len);
430 }
431
432 srcFile.checkError();
433 } catch (IOException e) {
434 success = false;
Steve McKay35645432016-01-20 15:09:35 -0800435 onFileFailed(src, "Exception thrown while copying from "
436 + src.derivedUri + " to " + dest.derivedUri + ".");
Steve McKay14e827a2016-01-06 18:32:13 -0800437
438 if (dstFile != null) {
439 try {
440 dstFile.closeWithError(e.getMessage());
441 } catch (IOException closeError) {
442 Log.e(TAG, "Error closing destination", closeError);
443 }
444 }
445 } finally {
446 // This also ensures the file descriptors are closed.
Steve McKay35645432016-01-20 15:09:35 -0800447 IoUtils.closeQuietly(in);
448 IoUtils.closeQuietly(out);
Steve McKay14e827a2016-01-06 18:32:13 -0800449 }
450
451 if (!success) {
Steve McKayecbf3c52016-01-13 17:17:39 -0800452 if (DEBUG) Log.d(TAG, "Cleaning up failed operation leftovers.");
Steve McKay14e827a2016-01-06 18:32:13 -0800453 canceller.cancel();
454 try {
Steve McKay35645432016-01-20 15:09:35 -0800455 DocumentsContract.deleteDocument(getClient(dest), dest.derivedUri);
Steve McKay14e827a2016-01-06 18:32:13 -0800456 } catch (RemoteException e) {
457 // RemoteExceptions usually signal that the connection is dead, so there's no
458 // point attempting to continue. Propagate the exception up so the copy job is
459 // cancelled.
Steve McKay35645432016-01-20 15:09:35 -0800460 Log.w(TAG, "Failed to cleanup after copy error: " + src.derivedUri, e);
Steve McKay14e827a2016-01-06 18:32:13 -0800461 throw e;
462 }
463 }
464
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +0900465 if (src.isVirtualDocument() && success) {
466 convertedFiles.add(src);
467 }
468
Steve McKay14e827a2016-01-06 18:32:13 -0800469 return success;
470 }
471
472 /**
473 * Calculates the cumulative size of all the documents in the list. Directories are recursed
474 * into and totaled up.
475 *
476 * @param srcs
477 * @return Size in bytes.
478 * @throws RemoteException
479 */
Steve McKay35645432016-01-20 15:09:35 -0800480 private long calculateSize(List<DocumentInfo> srcs)
Steve McKay14e827a2016-01-06 18:32:13 -0800481 throws RemoteException {
482 long result = 0;
483
484 for (DocumentInfo src : srcs) {
485 if (src.isDirectory()) {
486 // Directories need to be recursed into.
Steve McKay35645432016-01-20 15:09:35 -0800487 result += calculateFileSizesRecursively(getClient(src), src.derivedUri);
Steve McKay14e827a2016-01-06 18:32:13 -0800488 } else {
489 result += src.size;
490 }
491 }
492 return result;
493 }
494
495 /**
496 * Calculates (recursively) the cumulative size of all the files under the given directory.
497 *
498 * @throws RemoteException
499 */
500 private static long calculateFileSizesRecursively(
501 ContentProviderClient client, Uri uri) throws RemoteException {
502 final String authority = uri.getAuthority();
Steve McKayecbf3c52016-01-13 17:17:39 -0800503 final Uri queryUri = buildChildDocumentsUri(authority, getDocumentId(uri));
Steve McKay14e827a2016-01-06 18:32:13 -0800504 final String queryColumns[] = new String[] {
505 Document.COLUMN_DOCUMENT_ID,
506 Document.COLUMN_MIME_TYPE,
507 Document.COLUMN_SIZE
508 };
509
510 long result = 0;
511 Cursor cursor = null;
512 try {
513 cursor = client.query(queryUri, queryColumns, null, null, null);
514 while (cursor.moveToNext()) {
515 if (Document.MIME_TYPE_DIR.equals(
516 getCursorString(cursor, Document.COLUMN_MIME_TYPE))) {
517 // Recurse into directories.
Steve McKayecbf3c52016-01-13 17:17:39 -0800518 final Uri dirUri = buildDocumentUri(authority,
Steve McKay14e827a2016-01-06 18:32:13 -0800519 getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
520 result += calculateFileSizesRecursively(client, dirUri);
521 } else {
522 // This may return -1 if the size isn't defined. Ignore those cases.
523 long size = getCursorLong(cursor, Document.COLUMN_SIZE);
524 result += size > 0 ? size : 0;
525 }
526 }
527 } finally {
528 IoUtils.closeQuietly(cursor);
529 }
530
531 return result;
532 }
533
Steve McKay14e827a2016-01-06 18:32:13 -0800534 /**
535 * Returns true if {@code doc} is a descendant of {@code parentDoc}.
536 * @throws RemoteException
537 */
Steve McKay35645432016-01-20 15:09:35 -0800538 boolean isDescendentOf(DocumentInfo doc, DocumentInfo parent)
Steve McKay14e827a2016-01-06 18:32:13 -0800539 throws RemoteException {
Steve McKay35645432016-01-20 15:09:35 -0800540 if (parent.isDirectory() && doc.authority.equals(parent.authority)) {
541 return isChildDocument(getClient(doc), doc.derivedUri, parent.derivedUri);
Steve McKay14e827a2016-01-06 18:32:13 -0800542 }
543 return false;
544 }
Steve McKayecbf3c52016-01-13 17:17:39 -0800545
546 private void onFileFailed(DocumentInfo file, String msg) {
547 Log.w(TAG, msg);
548 onFileFailed(file);
549 }
Steve McKay35645432016-01-20 15:09:35 -0800550
551 @Override
552 public String toString() {
553 return new StringBuilder()
554 .append("CopyJob")
555 .append("{")
556 .append("id=" + id)
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900557 .append(", srcs=" + mSrcs)
Steve McKay35645432016-01-20 15:09:35 -0800558 .append(", destination=" + stack)
559 .append("}")
560 .toString();
561 }
Steve McKayecbf3c52016-01-13 17:17:39 -0800562}