blob: e5e66f824f47e8a9e79de6b915302e60b12a9941 [file] [log] [blame]
Steve McKayc83baa02016-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 McKaybbeba522016-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 Mikolajewskidd2b31c2016-01-22 16:22:51 +090024import static com.android.documentsui.OperationDialogFragment.DIALOG_TYPE_CONVERTED;
Steve McKayc83baa02016-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 Mikolajewskidd2b31c2016-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 McKayc83baa02016-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 Mikolajewskidd2b31c2016-01-22 16:22:51 +090037import android.app.PendingIntent;
Steve McKayc83baa02016-01-06 18:32:13 -080038import android.content.ContentProviderClient;
39import android.content.Context;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090040import android.content.Intent;
Steve McKayc83baa02016-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
53import com.android.documentsui.R;
54import com.android.documentsui.model.DocumentInfo;
55import com.android.documentsui.model.DocumentStack;
Steve McKaybbeba522016-01-13 17:17:39 -080056import com.android.documentsui.services.FileOperationService.OpType;
Steve McKayc83baa02016-01-06 18:32:13 -080057
58import libcore.io.IoUtils;
59
60import java.io.FileNotFoundException;
61import java.io.IOException;
62import java.io.InputStream;
63import java.io.OutputStream;
64import java.text.NumberFormat;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090065import java.util.ArrayList;
Steve McKayc83baa02016-01-06 18:32:13 -080066import java.util.List;
67
68class CopyJob extends Job {
69 private static final String TAG = "CopyJob";
70 private static final int PROGRESS_INTERVAL_MILLIS = 1000;
Steve McKay97b4be42016-01-20 15:09:35 -080071 final List<DocumentInfo> mSrcs;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090072 final ArrayList<DocumentInfo> convertedFiles = new ArrayList<>();
Steve McKayc83baa02016-01-06 18:32:13 -080073
74 private long mStartTime = -1;
75 private long mBatchSize;
76 private long mBytesCopied;
77 private long mLastNotificationTime;
78 // Speed estimation
79 private long mBytesCopiedSample;
80 private long mSampleTime;
81 private long mSpeed;
82 private long mRemainingTime;
83
84 /**
85 * Copies files to a destination identified by {@code destination}.
86 * @see @link {@link Job} constructor for most param descriptions.
87 *
88 * @param srcs List of files to be copied.
89 */
Steve McKaybbeba522016-01-13 17:17:39 -080090 CopyJob(Context service, Context appContext, Listener listener,
Steve McKay97b4be42016-01-20 15:09:35 -080091 String id, DocumentStack stack, List<DocumentInfo> srcs) {
92 super(service, appContext, listener, OPERATION_COPY, id, stack);
Steve McKaybbeba522016-01-13 17:17:39 -080093
94 checkArgument(!srcs.isEmpty());
Steve McKay97b4be42016-01-20 15:09:35 -080095 this.mSrcs = srcs;
Steve McKaybbeba522016-01-13 17:17:39 -080096 }
97
98 /**
99 * @see @link {@link Job} constructor for most param descriptions.
100 *
101 * @param srcs List of files to be copied.
102 */
103 CopyJob(Context service, Context appContext, Listener listener,
104 @OpType int opType, String id, DocumentStack destination, List<DocumentInfo> srcs) {
105 super(service, appContext, listener, opType, id, destination);
Steve McKayc83baa02016-01-06 18:32:13 -0800106
107 checkArgument(!srcs.isEmpty());
Steve McKay97b4be42016-01-20 15:09:35 -0800108 this.mSrcs = srcs;
Steve McKayc83baa02016-01-06 18:32:13 -0800109 }
110
111 @Override
112 Builder createProgressBuilder() {
113 return super.createProgressBuilder(
Steve McKaybbeba522016-01-13 17:17:39 -0800114 service.getString(R.string.copy_notification_title),
Steve McKayc83baa02016-01-06 18:32:13 -0800115 R.drawable.ic_menu_copy,
Steve McKaybbeba522016-01-13 17:17:39 -0800116 service.getString(android.R.string.cancel),
Steve McKayc83baa02016-01-06 18:32:13 -0800117 R.drawable.ic_cab_cancel);
118 }
119
120 @Override
121 public Notification getSetupNotification() {
Steve McKaybbeba522016-01-13 17:17:39 -0800122 return getSetupNotification(service.getString(R.string.copy_preparing));
Steve McKayc83baa02016-01-06 18:32:13 -0800123 }
124
125 public boolean shouldUpdateProgress() {
126 // Wait a while between updates :)
127 return elapsedRealtime() - mLastNotificationTime > PROGRESS_INTERVAL_MILLIS;
128 }
129
130 Notification getProgressNotification(@StringRes int msgId) {
131 double completed = (double) this.mBytesCopied / mBatchSize;
132 mProgressBuilder.setProgress(100, (int) (completed * 100), false);
133 mProgressBuilder.setContentInfo(
134 NumberFormat.getPercentInstance().format(completed));
135 if (mRemainingTime > 0) {
Steve McKaybbeba522016-01-13 17:17:39 -0800136 mProgressBuilder.setContentText(service.getString(msgId,
Steve McKayc83baa02016-01-06 18:32:13 -0800137 DateUtils.formatDuration(mRemainingTime)));
138 } else {
139 mProgressBuilder.setContentText(null);
140 }
141
142 // Remember when we last returned progress so we can provide an answer
143 // in shouldUpdateProgress.
144 mLastNotificationTime = elapsedRealtime();
145 return mProgressBuilder.build();
146 }
147
148 public Notification getProgressNotification() {
149 return getProgressNotification(R.string.copy_remaining);
150 }
151
152 void onBytesCopied(long numBytes) {
153 this.mBytesCopied += numBytes;
154 }
155
156 /**
157 * Generates an estimate of the remaining time in the copy.
158 */
159 void updateRemainingTimeEstimate() {
160 long elapsedTime = elapsedRealtime() - mStartTime;
161
162 final long sampleDuration = elapsedTime - mSampleTime;
163 final long sampleSpeed = ((mBytesCopied - mBytesCopiedSample) * 1000) / sampleDuration;
164 if (mSpeed == 0) {
165 mSpeed = sampleSpeed;
166 } else {
167 mSpeed = ((3 * mSpeed) + sampleSpeed) / 4;
168 }
169
170 if (mSampleTime > 0 && mSpeed > 0) {
171 mRemainingTime = ((mBatchSize - mBytesCopied) * 1000) / mSpeed;
172 } else {
173 mRemainingTime = 0;
174 }
175
176 mSampleTime = elapsedTime;
177 mBytesCopiedSample = mBytesCopied;
178 }
179
180 @Override
181 Notification getFailureNotification() {
182 return getFailureNotification(
183 R.plurals.copy_error_notification_title, R.drawable.ic_menu_copy);
184 }
185
186 @Override
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900187 Notification getWarningNotification() {
188 final Intent navigateIntent = buildNavigateIntent();
189 navigateIntent.putExtra(EXTRA_DIALOG_TYPE, DIALOG_TYPE_CONVERTED);
190 navigateIntent.putExtra(EXTRA_OPERATION, operationType);
191
192 navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, convertedFiles);
193
194 // TODO: Consider adding a dialog on tapping the notification with a list of
195 // converted files.
196 final Notification.Builder warningBuilder = new Notification.Builder(service)
197 .setContentTitle(service.getResources().getString(
198 R.string.notification_copy_files_converted_title))
199 .setContentText(service.getString(
200 R.string.notification_touch_for_details))
201 .setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent,
202 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
203 .setCategory(Notification.CATEGORY_ERROR)
204 .setSmallIcon(R.drawable.ic_menu_copy)
205 .setAutoCancel(true);
206 return warningBuilder.build();
207 }
208
209 @Override
Steve McKaybbeba522016-01-13 17:17:39 -0800210 void start() throws RemoteException {
Steve McKayc83baa02016-01-06 18:32:13 -0800211 mStartTime = elapsedRealtime();
212
Steve McKayc83baa02016-01-06 18:32:13 -0800213 // client
Steve McKay97b4be42016-01-20 15:09:35 -0800214 mBatchSize = calculateSize(mSrcs);
Steve McKayc83baa02016-01-06 18:32:13 -0800215
216 DocumentInfo srcInfo;
217 DocumentInfo dstInfo;
Steve McKay97b4be42016-01-20 15:09:35 -0800218 for (int i = 0; i < mSrcs.size() && !isCanceled(); ++i) {
219 srcInfo = mSrcs.get(i);
Steve McKayc83baa02016-01-06 18:32:13 -0800220 dstInfo = stack.peek();
221
222 // Guard unsupported recursive operation.
223 if (dstInfo.equals(srcInfo) || isDescendentOf(srcInfo, dstInfo)) {
Steve McKaybbeba522016-01-13 17:17:39 -0800224 onFileFailed(srcInfo,
225 "Skipping recursive operation on directory " + dstInfo.derivedUri + ".");
Steve McKayc83baa02016-01-06 18:32:13 -0800226 continue;
227 }
228
229 if (DEBUG) Log.d(TAG,
Steve McKaybbeba522016-01-13 17:17:39 -0800230 "Copying " + srcInfo.displayName + " (" + srcInfo.derivedUri + ")"
231 + " to " + dstInfo.displayName + " (" + dstInfo.derivedUri + ")");
Steve McKayc83baa02016-01-06 18:32:13 -0800232
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +0900233 processDocument(srcInfo, null, dstInfo);
Steve McKayc83baa02016-01-06 18:32:13 -0800234 }
235 }
236
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900237 @Override
238 boolean hasWarnings() {
239 return !convertedFiles.isEmpty();
240 }
241
Steve McKayc83baa02016-01-06 18:32:13 -0800242 /**
243 * Logs progress on the current copy operation. Displays/Updates the progress notification.
244 *
245 * @param bytesCopied
246 */
247 private void makeCopyProgress(long bytesCopied) {
248 onBytesCopied(bytesCopied);
249 if (shouldUpdateProgress()) {
250 updateRemainingTimeEstimate();
251 listener.onProgress(this);
252 }
253 }
254
255 /**
256 * Copies a the given document to the given location.
257 *
Steve McKay97b4be42016-01-20 15:09:35 -0800258 * @param src DocumentInfos for the documents to copy.
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +0900259 * @param srcParent DocumentInfo for the parent of the document to process.
Steve McKayc83baa02016-01-06 18:32:13 -0800260 * @param dstDirInfo The destination directory.
Steve McKayc83baa02016-01-06 18:32:13 -0800261 * @return True on success, false on failure.
262 * @throws RemoteException
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +0900263 *
264 * TODO: Stop passing srcParent, as it's not used for copy, but for move only.
Steve McKayc83baa02016-01-06 18:32:13 -0800265 */
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +0900266 boolean processDocument(DocumentInfo src, DocumentInfo srcParent,
267 DocumentInfo dstDirInfo) throws RemoteException {
Steve McKayc83baa02016-01-06 18:32:13 -0800268
269 // TODO: When optimized copy kicks in, we'll not making any progress updates.
270 // For now. Local storage isn't using optimized copy.
271
Tomasz Mikolajewski4aa89672016-01-21 10:00:33 +0900272 // When copying within the same provider, try to use optimized copying.
Steve McKayc83baa02016-01-06 18:32:13 -0800273 // If not supported, then fallback to byte-by-byte copy/move.
Steve McKay97b4be42016-01-20 15:09:35 -0800274 if (src.authority.equals(dstDirInfo.authority)) {
275 if ((src.flags & Document.FLAG_SUPPORTS_COPY) != 0) {
276 if (DocumentsContract.copyDocument(getClient(src), src.derivedUri,
Steve McKayc83baa02016-01-06 18:32:13 -0800277 dstDirInfo.derivedUri) == null) {
Steve McKay97b4be42016-01-20 15:09:35 -0800278 onFileFailed(src,
279 "Provider side copy failed for documents: " + src.derivedUri + ".");
Tomasz Mikolajewski4aa89672016-01-21 10:00:33 +0900280 return false;
Steve McKayc83baa02016-01-06 18:32:13 -0800281 }
Tomasz Mikolajewski4aa89672016-01-21 10:00:33 +0900282 return true;
Steve McKayc83baa02016-01-06 18:32:13 -0800283 }
284 }
285
286 // If we couldn't do an optimized copy...we fall back to vanilla byte copy.
Steve McKay97b4be42016-01-20 15:09:35 -0800287 return byteCopyDocument(src, dstDirInfo);
Steve McKayc83baa02016-01-06 18:32:13 -0800288 }
289
Steve McKay97b4be42016-01-20 15:09:35 -0800290 boolean byteCopyDocument(DocumentInfo src, DocumentInfo dest)
Steve McKayc83baa02016-01-06 18:32:13 -0800291 throws RemoteException {
292 final String dstMimeType;
293 final String dstDisplayName;
294
Steve McKay97b4be42016-01-20 15:09:35 -0800295 if (DEBUG) Log.d(TAG, "Doing byte copy of document: " + src);
Steve McKayc83baa02016-01-06 18:32:13 -0800296 // If the file is virtual, but can be converted to another format, then try to copy it
297 // as such format. Also, append an extension for the target mime type (if known).
Steve McKay97b4be42016-01-20 15:09:35 -0800298 if (src.isVirtualDocument()) {
Steve McKayc83baa02016-01-06 18:32:13 -0800299 final String[] streamTypes = getContentResolver().getStreamTypes(
Steve McKay97b4be42016-01-20 15:09:35 -0800300 src.derivedUri, "*/*");
Steve McKayc83baa02016-01-06 18:32:13 -0800301 if (streamTypes != null && streamTypes.length > 0) {
302 dstMimeType = streamTypes[0];
303 final String extension = MimeTypeMap.getSingleton().
304 getExtensionFromMimeType(dstMimeType);
Steve McKay97b4be42016-01-20 15:09:35 -0800305 dstDisplayName = src.displayName +
306 (extension != null ? "." + extension : src.displayName);
Steve McKayc83baa02016-01-06 18:32:13 -0800307 } else {
Steve McKay97b4be42016-01-20 15:09:35 -0800308 onFileFailed(src, "Cannot copy virtual file. No streamable formats available.");
Steve McKayc83baa02016-01-06 18:32:13 -0800309 return false;
310 }
311 } else {
Steve McKay97b4be42016-01-20 15:09:35 -0800312 dstMimeType = src.mimeType;
313 dstDisplayName = src.displayName;
Steve McKayc83baa02016-01-06 18:32:13 -0800314 }
315
316 // Create the target document (either a file or a directory), then copy recursively the
317 // contents (bytes or children).
Steve McKay97b4be42016-01-20 15:09:35 -0800318 final Uri dstUri = DocumentsContract.createDocument(
319 getClient(dest), dest.derivedUri, dstMimeType, dstDisplayName);
Steve McKayc83baa02016-01-06 18:32:13 -0800320 if (dstUri == null) {
321 // If this is a directory, the entire subdir will not be copied over.
Steve McKay97b4be42016-01-20 15:09:35 -0800322 onFileFailed(src,
Steve McKaybbeba522016-01-13 17:17:39 -0800323 "Couldn't create destination document " + dstDisplayName
Steve McKay97b4be42016-01-20 15:09:35 -0800324 + " in directory " + dest.displayName + ".");
Steve McKayc83baa02016-01-06 18:32:13 -0800325 return false;
326 }
327
328 DocumentInfo dstInfo = null;
329 try {
330 dstInfo = DocumentInfo.fromUri(getContentResolver(), dstUri);
331 } catch (FileNotFoundException e) {
Steve McKay97b4be42016-01-20 15:09:35 -0800332 onFileFailed(src,
Steve McKaybbeba522016-01-13 17:17:39 -0800333 "Could not load DocumentInfo for newly created file: " + dstUri + ".");
Steve McKayc83baa02016-01-06 18:32:13 -0800334 return false;
335 }
336
337 final boolean success;
Steve McKay97b4be42016-01-20 15:09:35 -0800338 if (Document.MIME_TYPE_DIR.equals(src.mimeType)) {
339 success = copyDirectoryHelper(src, dstInfo);
Steve McKayc83baa02016-01-06 18:32:13 -0800340 } else {
Steve McKay97b4be42016-01-20 15:09:35 -0800341 success = copyFileHelper(src, dstInfo, dstMimeType);
Steve McKayc83baa02016-01-06 18:32:13 -0800342 }
343
344 return success;
345 }
346
347 /**
348 * Handles recursion into a directory and copying its contents. Note that in linux terms, this
349 * does the equivalent of "cp src/* dst", not "cp -r src dst".
350 *
Steve McKay97b4be42016-01-20 15:09:35 -0800351 * @param srcDir Info of the directory to copy from. The routine will copy the directory's
Steve McKayc83baa02016-01-06 18:32:13 -0800352 * contents, not the directory itself.
Steve McKay97b4be42016-01-20 15:09:35 -0800353 * @param destDir Info of the directory to copy to. Must be created beforehand.
Steve McKayc83baa02016-01-06 18:32:13 -0800354 * @return True on success, false if some of the children failed to copy.
355 * @throws RemoteException
356 */
Steve McKay97b4be42016-01-20 15:09:35 -0800357 private boolean copyDirectoryHelper(DocumentInfo srcDir, DocumentInfo destDir)
Steve McKayc83baa02016-01-06 18:32:13 -0800358 throws RemoteException {
359 // Recurse into directories. Copy children into the new subdirectory.
360 final String queryColumns[] = new String[] {
361 Document.COLUMN_DISPLAY_NAME,
362 Document.COLUMN_DOCUMENT_ID,
363 Document.COLUMN_MIME_TYPE,
364 Document.COLUMN_SIZE,
365 Document.COLUMN_FLAGS
366 };
367 Cursor cursor = null;
368 boolean success = true;
369 try {
370 // Iterate over srcs in the directory; copy to the destination directory.
Steve McKay97b4be42016-01-20 15:09:35 -0800371 final Uri queryUri = buildChildDocumentsUri(srcDir.authority, srcDir.documentId);
372 cursor = getClient(srcDir).query(queryUri, queryColumns, null, null, null);
Steve McKaybbeba522016-01-13 17:17:39 -0800373 while (cursor.moveToNext() && !isCanceled()) {
Steve McKay97b4be42016-01-20 15:09:35 -0800374 DocumentInfo src = DocumentInfo.fromCursor(cursor, srcDir.authority);
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +0900375 success &= processDocument(src, srcDir, destDir);
Steve McKayc83baa02016-01-06 18:32:13 -0800376 }
377 } finally {
378 IoUtils.closeQuietly(cursor);
379 }
380
381 return success;
382 }
383
384 /**
385 * Handles copying a single file.
386 *
387 * @param srcUriInfo Info of the file to copy from.
388 * @param dstUriInfo Info of the *file* to copy to. Must be created beforehand.
389 * @param mimeType Mime type for the target. Can be different than source for virtual files.
390 * @return True on success, false on error.
391 * @throws RemoteException
392 */
Steve McKay97b4be42016-01-20 15:09:35 -0800393 private boolean copyFileHelper(DocumentInfo src, DocumentInfo dest, String mimeType)
Steve McKayc83baa02016-01-06 18:32:13 -0800394 throws RemoteException {
Steve McKayc83baa02016-01-06 18:32:13 -0800395 CancellationSignal canceller = new CancellationSignal();
396 ParcelFileDescriptor srcFile = null;
397 ParcelFileDescriptor dstFile = null;
Steve McKay97b4be42016-01-20 15:09:35 -0800398 InputStream in = null;
399 OutputStream out = null;
Steve McKayc83baa02016-01-06 18:32:13 -0800400
401 boolean success = true;
402 try {
403 // If the file is virtual, but can be converted to another format, then try to copy it
404 // as such format.
Steve McKay97b4be42016-01-20 15:09:35 -0800405 if (src.isVirtualDocument()) {
Steve McKayc83baa02016-01-06 18:32:13 -0800406 final AssetFileDescriptor srcFileAsAsset =
Steve McKay97b4be42016-01-20 15:09:35 -0800407 getClient(src).openTypedAssetFileDescriptor(
408 src.derivedUri, mimeType, null, canceller);
Steve McKayc83baa02016-01-06 18:32:13 -0800409 srcFile = srcFileAsAsset.getParcelFileDescriptor();
Steve McKay97b4be42016-01-20 15:09:35 -0800410 in = new AssetFileDescriptor.AutoCloseInputStream(srcFileAsAsset);
Steve McKayc83baa02016-01-06 18:32:13 -0800411 } else {
Steve McKay97b4be42016-01-20 15:09:35 -0800412 srcFile = getClient(src).openFile(src.derivedUri, "r", canceller);
413 in = new ParcelFileDescriptor.AutoCloseInputStream(srcFile);
Steve McKayc83baa02016-01-06 18:32:13 -0800414 }
415
Steve McKay97b4be42016-01-20 15:09:35 -0800416 dstFile = getClient(dest).openFile(dest.derivedUri, "w", canceller);
417 out = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);
Steve McKayc83baa02016-01-06 18:32:13 -0800418
Steve McKaybbeba522016-01-13 17:17:39 -0800419 byte[] buffer = new byte[32 * 1024];
Steve McKayc83baa02016-01-06 18:32:13 -0800420 int len;
Steve McKay97b4be42016-01-20 15:09:35 -0800421 while ((len = in.read(buffer)) != -1) {
Steve McKayc83baa02016-01-06 18:32:13 -0800422 if (isCanceled()) {
423 if (DEBUG) Log.d(TAG, "Canceled copy mid-copy. Id:" + id);
424 success = false;
425 break;
426 }
Steve McKay97b4be42016-01-20 15:09:35 -0800427 out.write(buffer, 0, len);
Steve McKayc83baa02016-01-06 18:32:13 -0800428 makeCopyProgress(len);
429 }
430
431 srcFile.checkError();
432 } catch (IOException e) {
433 success = false;
Steve McKay97b4be42016-01-20 15:09:35 -0800434 onFileFailed(src, "Exception thrown while copying from "
435 + src.derivedUri + " to " + dest.derivedUri + ".");
Steve McKayc83baa02016-01-06 18:32:13 -0800436
437 if (dstFile != null) {
438 try {
439 dstFile.closeWithError(e.getMessage());
440 } catch (IOException closeError) {
441 Log.e(TAG, "Error closing destination", closeError);
442 }
443 }
444 } finally {
445 // This also ensures the file descriptors are closed.
Steve McKay97b4be42016-01-20 15:09:35 -0800446 IoUtils.closeQuietly(in);
447 IoUtils.closeQuietly(out);
Steve McKayc83baa02016-01-06 18:32:13 -0800448 }
449
450 if (!success) {
Steve McKaybbeba522016-01-13 17:17:39 -0800451 if (DEBUG) Log.d(TAG, "Cleaning up failed operation leftovers.");
Steve McKayc83baa02016-01-06 18:32:13 -0800452 canceller.cancel();
453 try {
Steve McKay97b4be42016-01-20 15:09:35 -0800454 DocumentsContract.deleteDocument(getClient(dest), dest.derivedUri);
Steve McKayc83baa02016-01-06 18:32:13 -0800455 } catch (RemoteException e) {
456 // RemoteExceptions usually signal that the connection is dead, so there's no
457 // point attempting to continue. Propagate the exception up so the copy job is
458 // cancelled.
Steve McKay97b4be42016-01-20 15:09:35 -0800459 Log.w(TAG, "Failed to cleanup after copy error: " + src.derivedUri, e);
Steve McKayc83baa02016-01-06 18:32:13 -0800460 throw e;
461 }
462 }
463
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900464 if (src.isVirtualDocument() && success) {
465 convertedFiles.add(src);
466 }
467
Steve McKayc83baa02016-01-06 18:32:13 -0800468 return success;
469 }
470
471 /**
472 * Calculates the cumulative size of all the documents in the list. Directories are recursed
473 * into and totaled up.
474 *
475 * @param srcs
476 * @return Size in bytes.
477 * @throws RemoteException
478 */
Steve McKay97b4be42016-01-20 15:09:35 -0800479 private long calculateSize(List<DocumentInfo> srcs)
Steve McKayc83baa02016-01-06 18:32:13 -0800480 throws RemoteException {
481 long result = 0;
482
483 for (DocumentInfo src : srcs) {
484 if (src.isDirectory()) {
485 // Directories need to be recursed into.
Steve McKay97b4be42016-01-20 15:09:35 -0800486 result += calculateFileSizesRecursively(getClient(src), src.derivedUri);
Steve McKayc83baa02016-01-06 18:32:13 -0800487 } else {
488 result += src.size;
489 }
490 }
491 return result;
492 }
493
494 /**
495 * Calculates (recursively) the cumulative size of all the files under the given directory.
496 *
497 * @throws RemoteException
498 */
499 private static long calculateFileSizesRecursively(
500 ContentProviderClient client, Uri uri) throws RemoteException {
501 final String authority = uri.getAuthority();
Steve McKaybbeba522016-01-13 17:17:39 -0800502 final Uri queryUri = buildChildDocumentsUri(authority, getDocumentId(uri));
Steve McKayc83baa02016-01-06 18:32:13 -0800503 final String queryColumns[] = new String[] {
504 Document.COLUMN_DOCUMENT_ID,
505 Document.COLUMN_MIME_TYPE,
506 Document.COLUMN_SIZE
507 };
508
509 long result = 0;
510 Cursor cursor = null;
511 try {
512 cursor = client.query(queryUri, queryColumns, null, null, null);
513 while (cursor.moveToNext()) {
514 if (Document.MIME_TYPE_DIR.equals(
515 getCursorString(cursor, Document.COLUMN_MIME_TYPE))) {
516 // Recurse into directories.
Steve McKaybbeba522016-01-13 17:17:39 -0800517 final Uri dirUri = buildDocumentUri(authority,
Steve McKayc83baa02016-01-06 18:32:13 -0800518 getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
519 result += calculateFileSizesRecursively(client, dirUri);
520 } else {
521 // This may return -1 if the size isn't defined. Ignore those cases.
522 long size = getCursorLong(cursor, Document.COLUMN_SIZE);
523 result += size > 0 ? size : 0;
524 }
525 }
526 } finally {
527 IoUtils.closeQuietly(cursor);
528 }
529
530 return result;
531 }
532
Steve McKayc83baa02016-01-06 18:32:13 -0800533 /**
534 * Returns true if {@code doc} is a descendant of {@code parentDoc}.
535 * @throws RemoteException
536 */
Steve McKay97b4be42016-01-20 15:09:35 -0800537 boolean isDescendentOf(DocumentInfo doc, DocumentInfo parent)
Steve McKayc83baa02016-01-06 18:32:13 -0800538 throws RemoteException {
Steve McKay97b4be42016-01-20 15:09:35 -0800539 if (parent.isDirectory() && doc.authority.equals(parent.authority)) {
540 return isChildDocument(getClient(doc), doc.derivedUri, parent.derivedUri);
Steve McKayc83baa02016-01-06 18:32:13 -0800541 }
542 return false;
543 }
Steve McKaybbeba522016-01-13 17:17:39 -0800544
545 private void onFileFailed(DocumentInfo file, String msg) {
546 Log.w(TAG, msg);
547 onFileFailed(file);
548 }
Steve McKay97b4be42016-01-20 15:09:35 -0800549
550 @Override
551 public String toString() {
552 return new StringBuilder()
553 .append("CopyJob")
554 .append("{")
555 .append("id=" + id)
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +0900556 .append(", srcs=" + mSrcs)
Steve McKay97b4be42016-01-20 15:09:35 -0800557 .append(", destination=" + stack)
558 .append("}")
559 .toString();
560 }
Steve McKaybbeba522016-01-13 17:17:39 -0800561}