blob: f10af4378c21907f7dc008258d6e5511edfc31d3 [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;
Steve McKay14e827a2016-01-06 18:32:13 -080032
33import android.annotation.StringRes;
34import android.app.Notification;
35import android.app.Notification.Builder;
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +090036import android.app.PendingIntent;
Steve McKay14e827a2016-01-06 18:32:13 -080037import android.content.ContentProviderClient;
38import android.content.Context;
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +090039import android.content.Intent;
Steve McKay14e827a2016-01-06 18:32:13 -080040import android.content.res.AssetFileDescriptor;
41import android.database.Cursor;
42import android.net.Uri;
43import android.os.CancellationSignal;
44import android.os.ParcelFileDescriptor;
45import android.os.RemoteException;
46import android.provider.DocumentsContract;
47import android.provider.DocumentsContract.Document;
Daichi Hironof4e7fa82016-03-28 16:07:45 +090048import android.system.ErrnoException;
49import android.system.Os;
Steve McKay14e827a2016-01-06 18:32:13 -080050import android.text.format.DateUtils;
51import android.util.Log;
52import android.webkit.MimeTypeMap;
53
Ben Kwad5b2af12016-01-28 16:39:57 -080054import com.android.documentsui.Metrics;
Steve McKay14e827a2016-01-06 18:32:13 -080055import com.android.documentsui.R;
56import com.android.documentsui.model.DocumentInfo;
57import com.android.documentsui.model.DocumentStack;
Steve McKayecbf3c52016-01-13 17:17:39 -080058import com.android.documentsui.services.FileOperationService.OpType;
Steve McKay14e827a2016-01-06 18:32:13 -080059
60import libcore.io.IoUtils;
61
62import java.io.FileNotFoundException;
63import java.io.IOException;
64import java.io.InputStream;
65import java.io.OutputStream;
66import java.text.NumberFormat;
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +090067import java.util.ArrayList;
Steve McKay14e827a2016-01-06 18:32:13 -080068import java.util.List;
69
70class CopyJob extends Job {
Steve McKay7a3b8112016-02-23 10:06:50 -080071
Steve McKay14e827a2016-01-06 18:32:13 -080072 private static final String TAG = "CopyJob";
Steve McKay7a3b8112016-02-23 10:06:50 -080073 private static final int PROGRESS_INTERVAL_MILLIS = 500;
74
Steve McKay35645432016-01-20 15:09:35 -080075 final List<DocumentInfo> mSrcs;
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +090076 final ArrayList<DocumentInfo> convertedFiles = new ArrayList<>();
Steve McKay14e827a2016-01-06 18:32:13 -080077
78 private long mStartTime = -1;
Steve McKay7a3b8112016-02-23 10:06:50 -080079
Steve McKay14e827a2016-01-06 18:32:13 -080080 private long mBatchSize;
81 private long mBytesCopied;
82 private long mLastNotificationTime;
83 // Speed estimation
84 private long mBytesCopiedSample;
85 private long mSampleTime;
86 private long mSpeed;
87 private long mRemainingTime;
88
89 /**
90 * Copies files to a destination identified by {@code destination}.
91 * @see @link {@link Job} constructor for most param descriptions.
92 *
93 * @param srcs List of files to be copied.
94 */
Steve McKayecbf3c52016-01-13 17:17:39 -080095 CopyJob(Context service, Context appContext, Listener listener,
Steve McKay35645432016-01-20 15:09:35 -080096 String id, DocumentStack stack, List<DocumentInfo> srcs) {
97 super(service, appContext, listener, OPERATION_COPY, id, stack);
Steve McKayecbf3c52016-01-13 17:17:39 -080098
Steve McKaya1f76802016-02-25 13:34:03 -080099 assert(!srcs.isEmpty());
Steve McKay35645432016-01-20 15:09:35 -0800100 this.mSrcs = srcs;
Steve McKayecbf3c52016-01-13 17:17:39 -0800101 }
102
103 /**
104 * @see @link {@link Job} constructor for most param descriptions.
105 *
106 * @param srcs List of files to be copied.
107 */
108 CopyJob(Context service, Context appContext, Listener listener,
109 @OpType int opType, String id, DocumentStack destination, List<DocumentInfo> srcs) {
110 super(service, appContext, listener, opType, id, destination);
Steve McKay14e827a2016-01-06 18:32:13 -0800111
Steve McKaya1f76802016-02-25 13:34:03 -0800112 assert(!srcs.isEmpty());
Steve McKay35645432016-01-20 15:09:35 -0800113 this.mSrcs = srcs;
Steve McKay14e827a2016-01-06 18:32:13 -0800114 }
115
116 @Override
117 Builder createProgressBuilder() {
118 return super.createProgressBuilder(
Steve McKayecbf3c52016-01-13 17:17:39 -0800119 service.getString(R.string.copy_notification_title),
Steve McKay14e827a2016-01-06 18:32:13 -0800120 R.drawable.ic_menu_copy,
Steve McKayecbf3c52016-01-13 17:17:39 -0800121 service.getString(android.R.string.cancel),
Steve McKay14e827a2016-01-06 18:32:13 -0800122 R.drawable.ic_cab_cancel);
123 }
124
125 @Override
126 public Notification getSetupNotification() {
Steve McKayecbf3c52016-01-13 17:17:39 -0800127 return getSetupNotification(service.getString(R.string.copy_preparing));
Steve McKay14e827a2016-01-06 18:32:13 -0800128 }
129
130 public boolean shouldUpdateProgress() {
131 // Wait a while between updates :)
132 return elapsedRealtime() - mLastNotificationTime > PROGRESS_INTERVAL_MILLIS;
133 }
134
135 Notification getProgressNotification(@StringRes int msgId) {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900136 if (mBatchSize >= 0) {
137 double completed = (double) this.mBytesCopied / mBatchSize;
138 mProgressBuilder.setProgress(100, (int) (completed * 100), false);
139 mProgressBuilder.setContentInfo(
140 NumberFormat.getPercentInstance().format(completed));
141 } else {
142 // If the total file size failed to compute on some files, then show
143 // an indeterminate spinner. CopyJob would most likely fail on those
144 // files while copying, but would continue with another files.
145 // Also, if the total size is 0 bytes, show an indeterminate spinner.
146 mProgressBuilder.setProgress(0, 0, true);
147 }
148
Steve McKay14e827a2016-01-06 18:32:13 -0800149 if (mRemainingTime > 0) {
Steve McKayecbf3c52016-01-13 17:17:39 -0800150 mProgressBuilder.setContentText(service.getString(msgId,
Steve McKay14e827a2016-01-06 18:32:13 -0800151 DateUtils.formatDuration(mRemainingTime)));
152 } else {
153 mProgressBuilder.setContentText(null);
154 }
155
156 // Remember when we last returned progress so we can provide an answer
157 // in shouldUpdateProgress.
158 mLastNotificationTime = elapsedRealtime();
159 return mProgressBuilder.build();
160 }
161
162 public Notification getProgressNotification() {
163 return getProgressNotification(R.string.copy_remaining);
164 }
165
166 void onBytesCopied(long numBytes) {
167 this.mBytesCopied += numBytes;
168 }
169
170 /**
171 * Generates an estimate of the remaining time in the copy.
172 */
173 void updateRemainingTimeEstimate() {
174 long elapsedTime = elapsedRealtime() - mStartTime;
175
176 final long sampleDuration = elapsedTime - mSampleTime;
177 final long sampleSpeed = ((mBytesCopied - mBytesCopiedSample) * 1000) / sampleDuration;
178 if (mSpeed == 0) {
179 mSpeed = sampleSpeed;
180 } else {
181 mSpeed = ((3 * mSpeed) + sampleSpeed) / 4;
182 }
183
184 if (mSampleTime > 0 && mSpeed > 0) {
185 mRemainingTime = ((mBatchSize - mBytesCopied) * 1000) / mSpeed;
186 } else {
187 mRemainingTime = 0;
188 }
189
190 mSampleTime = elapsedTime;
191 mBytesCopiedSample = mBytesCopied;
192 }
193
194 @Override
195 Notification getFailureNotification() {
196 return getFailureNotification(
197 R.plurals.copy_error_notification_title, R.drawable.ic_menu_copy);
198 }
199
200 @Override
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +0900201 Notification getWarningNotification() {
Tomasz Mikolajewskicd270152016-02-01 12:01:14 +0900202 final Intent navigateIntent = buildNavigateIntent(INTENT_TAG_WARNING);
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +0900203 navigateIntent.putExtra(EXTRA_DIALOG_TYPE, DIALOG_TYPE_CONVERTED);
204 navigateIntent.putExtra(EXTRA_OPERATION, operationType);
205
206 navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, convertedFiles);
207
208 // TODO: Consider adding a dialog on tapping the notification with a list of
209 // converted files.
210 final Notification.Builder warningBuilder = new Notification.Builder(service)
211 .setContentTitle(service.getResources().getString(
212 R.string.notification_copy_files_converted_title))
213 .setContentText(service.getString(
214 R.string.notification_touch_for_details))
215 .setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent,
216 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
217 .setCategory(Notification.CATEGORY_ERROR)
218 .setSmallIcon(R.drawable.ic_menu_copy)
219 .setAutoCancel(true);
220 return warningBuilder.build();
221 }
222
223 @Override
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900224 void start() {
Steve McKay14e827a2016-01-06 18:32:13 -0800225 mStartTime = elapsedRealtime();
226
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900227 try {
228 mBatchSize = calculateSize(mSrcs);
229 } catch (ResourceException e) {
230 Log.w(TAG, "Failed to calculate total size. Copying without progress.");
231 mBatchSize = -1;
232 }
Steve McKay14e827a2016-01-06 18:32:13 -0800233
234 DocumentInfo srcInfo;
Ben Kwad5b2af12016-01-28 16:39:57 -0800235 DocumentInfo dstInfo = stack.peek();
Steve McKay35645432016-01-20 15:09:35 -0800236 for (int i = 0; i < mSrcs.size() && !isCanceled(); ++i) {
237 srcInfo = mSrcs.get(i);
Steve McKay14e827a2016-01-06 18:32:13 -0800238
239 // Guard unsupported recursive operation.
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900240 try {
241 if (dstInfo.equals(srcInfo) || isDescendentOf(srcInfo, dstInfo)) {
242 throw new ResourceException("Cannot copy to itself recursively.");
243 }
244 } catch (ResourceException e) {
245 Log.e(TAG, e.toString());
246 onFileFailed(srcInfo);
Steve McKay14e827a2016-01-06 18:32:13 -0800247 continue;
248 }
249
250 if (DEBUG) Log.d(TAG,
Steve McKayecbf3c52016-01-13 17:17:39 -0800251 "Copying " + srcInfo.displayName + " (" + srcInfo.derivedUri + ")"
252 + " to " + dstInfo.displayName + " (" + dstInfo.derivedUri + ")");
Steve McKay14e827a2016-01-06 18:32:13 -0800253
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900254 try {
255 processDocument(srcInfo, null, dstInfo);
256 } catch (ResourceException e) {
257 Log.e(TAG, e.toString());
258 onFileFailed(srcInfo);
259 }
Steve McKay14e827a2016-01-06 18:32:13 -0800260 }
Ben Kwad5b2af12016-01-28 16:39:57 -0800261 Metrics.logFileOperation(service, operationType, mSrcs, dstInfo);
Steve McKay14e827a2016-01-06 18:32:13 -0800262 }
263
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +0900264 @Override
265 boolean hasWarnings() {
266 return !convertedFiles.isEmpty();
267 }
268
Steve McKay14e827a2016-01-06 18:32:13 -0800269 /**
270 * Logs progress on the current copy operation. Displays/Updates the progress notification.
271 *
272 * @param bytesCopied
273 */
274 private void makeCopyProgress(long bytesCopied) {
275 onBytesCopied(bytesCopied);
276 if (shouldUpdateProgress()) {
277 updateRemainingTimeEstimate();
278 listener.onProgress(this);
279 }
280 }
281
282 /**
283 * Copies a the given document to the given location.
284 *
Steve McKay35645432016-01-20 15:09:35 -0800285 * @param src DocumentInfos for the documents to copy.
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900286 * @param srcParent DocumentInfo for the parent of the document to process.
Steve McKay14e827a2016-01-06 18:32:13 -0800287 * @param dstDirInfo The destination directory.
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900288 * @throws ResourceException
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900289 *
290 * TODO: Stop passing srcParent, as it's not used for copy, but for move only.
Steve McKay14e827a2016-01-06 18:32:13 -0800291 */
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900292 void processDocument(DocumentInfo src, DocumentInfo srcParent,
293 DocumentInfo dstDirInfo) throws ResourceException {
Steve McKay14e827a2016-01-06 18:32:13 -0800294
295 // TODO: When optimized copy kicks in, we'll not making any progress updates.
296 // For now. Local storage isn't using optimized copy.
297
Tomasz Mikolajewski67048082016-01-21 10:00:33 +0900298 // When copying within the same provider, try to use optimized copying.
Steve McKay14e827a2016-01-06 18:32:13 -0800299 // If not supported, then fallback to byte-by-byte copy/move.
Steve McKay35645432016-01-20 15:09:35 -0800300 if (src.authority.equals(dstDirInfo.authority)) {
301 if ((src.flags & Document.FLAG_SUPPORTS_COPY) != 0) {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900302 try {
303 if (DocumentsContract.copyDocument(getClient(src), src.derivedUri,
Tomasz Mikolajewski1008a112016-03-02 17:41:40 +0900304 dstDirInfo.derivedUri) != null) {
305 return;
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900306 }
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900307 } catch (RemoteException | RuntimeException e) {
Tomasz Mikolajewski1008a112016-03-02 17:41:40 +0900308 Log.e(TAG, "Provider side copy failed for: " + src.derivedUri
309 + " due to an exception: " + e);
Steve McKay14e827a2016-01-06 18:32:13 -0800310 }
Tomasz Mikolajewski1008a112016-03-02 17:41:40 +0900311 // If optimized copy fails, then fallback to byte-by-byte copy.
312 if (DEBUG) Log.d(TAG, "Fallback to byte-by-byte copy for: " + src.derivedUri);
Steve McKay14e827a2016-01-06 18:32:13 -0800313 }
314 }
315
316 // If we couldn't do an optimized copy...we fall back to vanilla byte copy.
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900317 byteCopyDocument(src, dstDirInfo);
Steve McKay14e827a2016-01-06 18:32:13 -0800318 }
319
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900320 void byteCopyDocument(DocumentInfo src, DocumentInfo dest) throws ResourceException {
Steve McKay14e827a2016-01-06 18:32:13 -0800321 final String dstMimeType;
322 final String dstDisplayName;
323
Steve McKay35645432016-01-20 15:09:35 -0800324 if (DEBUG) Log.d(TAG, "Doing byte copy of document: " + src);
Steve McKay14e827a2016-01-06 18:32:13 -0800325 // If the file is virtual, but can be converted to another format, then try to copy it
326 // as such format. Also, append an extension for the target mime type (if known).
Steve McKay35645432016-01-20 15:09:35 -0800327 if (src.isVirtualDocument()) {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900328 String[] streamTypes = null;
329 try {
330 streamTypes = getContentResolver().getStreamTypes(src.derivedUri, "*/*");
331 } catch (RuntimeException e) {
332 throw new ResourceException(
333 "Failed to obtain streamable types for %s due to an exception.",
334 src.derivedUri, e);
335 }
Steve McKay14e827a2016-01-06 18:32:13 -0800336 if (streamTypes != null && streamTypes.length > 0) {
337 dstMimeType = streamTypes[0];
338 final String extension = MimeTypeMap.getSingleton().
339 getExtensionFromMimeType(dstMimeType);
Steve McKay35645432016-01-20 15:09:35 -0800340 dstDisplayName = src.displayName +
341 (extension != null ? "." + extension : src.displayName);
Steve McKay14e827a2016-01-06 18:32:13 -0800342 } else {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900343 throw new ResourceException("Cannot copy virtual file %s. No streamable formats "
344 + "available.", src.derivedUri);
Steve McKay14e827a2016-01-06 18:32:13 -0800345 }
346 } else {
Steve McKay35645432016-01-20 15:09:35 -0800347 dstMimeType = src.mimeType;
348 dstDisplayName = src.displayName;
Steve McKay14e827a2016-01-06 18:32:13 -0800349 }
350
351 // Create the target document (either a file or a directory), then copy recursively the
352 // contents (bytes or children).
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900353 Uri dstUri = null;
354 try {
355 dstUri = DocumentsContract.createDocument(
356 getClient(dest), dest.derivedUri, dstMimeType, dstDisplayName);
357 } catch (RemoteException | RuntimeException e) {
358 throw new ResourceException(
359 "Couldn't create destination document " + dstDisplayName + " in directory %s "
360 + "due to an exception.", dest.derivedUri, e);
361 }
Steve McKay14e827a2016-01-06 18:32:13 -0800362 if (dstUri == null) {
363 // If this is a directory, the entire subdir will not be copied over.
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900364 throw new ResourceException(
365 "Couldn't create destination document " + dstDisplayName + " in directory %s.",
366 dest.derivedUri);
Steve McKay14e827a2016-01-06 18:32:13 -0800367 }
368
369 DocumentInfo dstInfo = null;
370 try {
371 dstInfo = DocumentInfo.fromUri(getContentResolver(), dstUri);
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900372 } catch (FileNotFoundException | RuntimeException e) {
373 throw new ResourceException("Could not load DocumentInfo for newly created file %s.",
374 dstUri);
Steve McKay14e827a2016-01-06 18:32:13 -0800375 }
376
Steve McKay35645432016-01-20 15:09:35 -0800377 if (Document.MIME_TYPE_DIR.equals(src.mimeType)) {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900378 copyDirectoryHelper(src, dstInfo);
Steve McKay14e827a2016-01-06 18:32:13 -0800379 } else {
Tomasz Mikolajewskidb875432016-02-23 15:12:54 +0900380 copyFileHelper(src, dstInfo, dest, dstMimeType);
Steve McKay14e827a2016-01-06 18:32:13 -0800381 }
Steve McKay14e827a2016-01-06 18:32:13 -0800382 }
383
384 /**
385 * Handles recursion into a directory and copying its contents. Note that in linux terms, this
386 * does the equivalent of "cp src/* dst", not "cp -r src dst".
387 *
Steve McKay35645432016-01-20 15:09:35 -0800388 * @param srcDir Info of the directory to copy from. The routine will copy the directory's
Steve McKay14e827a2016-01-06 18:32:13 -0800389 * contents, not the directory itself.
Steve McKay35645432016-01-20 15:09:35 -0800390 * @param destDir Info of the directory to copy to. Must be created beforehand.
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900391 * @throws ResourceException
Steve McKay14e827a2016-01-06 18:32:13 -0800392 */
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900393 private void copyDirectoryHelper(DocumentInfo srcDir, DocumentInfo destDir)
394 throws ResourceException {
Steve McKay14e827a2016-01-06 18:32:13 -0800395 // Recurse into directories. Copy children into the new subdirectory.
396 final String queryColumns[] = new String[] {
397 Document.COLUMN_DISPLAY_NAME,
398 Document.COLUMN_DOCUMENT_ID,
399 Document.COLUMN_MIME_TYPE,
400 Document.COLUMN_SIZE,
401 Document.COLUMN_FLAGS
402 };
403 Cursor cursor = null;
404 boolean success = true;
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900405 // Iterate over srcs in the directory; copy to the destination directory.
406 final Uri queryUri = buildChildDocumentsUri(srcDir.authority, srcDir.documentId);
Steve McKay14e827a2016-01-06 18:32:13 -0800407 try {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900408 try {
409 cursor = getClient(srcDir).query(queryUri, queryColumns, null, null, null);
410 } catch (RemoteException | RuntimeException e) {
411 throw new ResourceException("Failed to query children of %s due to an exception.",
412 srcDir.derivedUri, e);
Steve McKay14e827a2016-01-06 18:32:13 -0800413 }
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900414
415 DocumentInfo src;
416 while (cursor.moveToNext() && !isCanceled()) {
417 try {
418 src = DocumentInfo.fromCursor(cursor, srcDir.authority);
419 processDocument(src, srcDir, destDir);
420 } catch (RuntimeException e) {
421 Log.e(TAG, "Failed to recursively process a file %s due to an exception."
422 .format(srcDir.derivedUri.toString()), e);
423 success = false;
424 }
425 }
426 } catch (RuntimeException e) {
427 Log.e(TAG, "Failed to copy a file %s to %s. "
428 .format(srcDir.derivedUri.toString(), destDir.derivedUri.toString()), e);
429 success = false;
Steve McKay14e827a2016-01-06 18:32:13 -0800430 } finally {
431 IoUtils.closeQuietly(cursor);
432 }
433
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900434 if (!success) {
435 throw new RuntimeException("Some files failed to copy during a recursive "
436 + "directory copy.");
437 }
Steve McKay14e827a2016-01-06 18:32:13 -0800438 }
439
440 /**
441 * Handles copying a single file.
442 *
Tomasz Mikolajewskidb875432016-02-23 15:12:54 +0900443 * @param src Info of the file to copy from.
444 * @param dest Info of the *file* to copy to. Must be created beforehand.
445 * @param destParent Info of the parent of the destination.
Steve McKay14e827a2016-01-06 18:32:13 -0800446 * @param mimeType Mime type for the target. Can be different than source for virtual files.
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900447 * @throws ResourceException
Steve McKay14e827a2016-01-06 18:32:13 -0800448 */
Tomasz Mikolajewskidb875432016-02-23 15:12:54 +0900449 private void copyFileHelper(DocumentInfo src, DocumentInfo dest, DocumentInfo destParent,
450 String mimeType) throws ResourceException {
Steve McKay14e827a2016-01-06 18:32:13 -0800451 CancellationSignal canceller = new CancellationSignal();
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900452 AssetFileDescriptor srcFileAsAsset = null;
Steve McKay14e827a2016-01-06 18:32:13 -0800453 ParcelFileDescriptor srcFile = null;
454 ParcelFileDescriptor dstFile = null;
Steve McKay35645432016-01-20 15:09:35 -0800455 InputStream in = null;
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900456 ParcelFileDescriptor.AutoCloseOutputStream out = null;
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900457 boolean success = false;
Steve McKay14e827a2016-01-06 18:32:13 -0800458
Steve McKay14e827a2016-01-06 18:32:13 -0800459 try {
460 // If the file is virtual, but can be converted to another format, then try to copy it
461 // as such format.
Steve McKay35645432016-01-20 15:09:35 -0800462 if (src.isVirtualDocument()) {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900463 try {
464 srcFileAsAsset = getClient(src).openTypedAssetFileDescriptor(
Steve McKay35645432016-01-20 15:09:35 -0800465 src.derivedUri, mimeType, null, canceller);
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900466 } catch (FileNotFoundException | RemoteException | RuntimeException e) {
467 throw new ResourceException("Failed to open a file as asset for %s due to an "
468 + "exception.", src.derivedUri, e);
469 }
Steve McKay14e827a2016-01-06 18:32:13 -0800470 srcFile = srcFileAsAsset.getParcelFileDescriptor();
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900471 try {
472 in = new AssetFileDescriptor.AutoCloseInputStream(srcFileAsAsset);
473 } catch (IOException e) {
474 throw new ResourceException("Failed to open a file input stream for %s due "
475 + "an exception.", src.derivedUri, e);
476 }
Steve McKay14e827a2016-01-06 18:32:13 -0800477 } else {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900478 try {
479 srcFile = getClient(src).openFile(src.derivedUri, "r", canceller);
480 } catch (FileNotFoundException | RemoteException | RuntimeException e) {
481 throw new ResourceException(
482 "Failed to open a file for %s due to an exception.", src.derivedUri, e);
483 }
Steve McKay35645432016-01-20 15:09:35 -0800484 in = new ParcelFileDescriptor.AutoCloseInputStream(srcFile);
Steve McKay14e827a2016-01-06 18:32:13 -0800485 }
486
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900487 try {
488 dstFile = getClient(dest).openFile(dest.derivedUri, "w", canceller);
489 } catch (FileNotFoundException | RemoteException | RuntimeException e) {
490 throw new ResourceException("Failed to open the destination file %s for writing "
491 + "due to an exception.", dest.derivedUri, e);
492 }
Steve McKay35645432016-01-20 15:09:35 -0800493 out = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);
Steve McKay14e827a2016-01-06 18:32:13 -0800494
Steve McKayecbf3c52016-01-13 17:17:39 -0800495 byte[] buffer = new byte[32 * 1024];
Steve McKay14e827a2016-01-06 18:32:13 -0800496 int len;
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900497 try {
498 while ((len = in.read(buffer)) != -1) {
499 if (isCanceled()) {
Steve McKay7a3b8112016-02-23 10:06:50 -0800500 if (DEBUG) Log.d(TAG, "Canceled copy mid-copy of: " + src.derivedUri);
501 return;
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900502 }
503 out.write(buffer, 0, len);
504 makeCopyProgress(len);
Steve McKay14e827a2016-01-06 18:32:13 -0800505 }
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900506
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900507 // Need to invoke IoUtils.close explicitly to avoid from ignoring errors at flush.
508 IoUtils.close(dstFile.getFileDescriptor());
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900509 srcFile.checkError();
510 } catch (IOException e) {
511 throw new ResourceException(
512 "Failed to copy bytes from %s to %s due to an IO exception.",
513 src.derivedUri, dest.derivedUri, e);
Steve McKay14e827a2016-01-06 18:32:13 -0800514 }
515
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900516 if (src.isVirtualDocument()) {
517 convertedFiles.add(src);
Steve McKay14e827a2016-01-06 18:32:13 -0800518 }
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900519
520 success = true;
Steve McKay14e827a2016-01-06 18:32:13 -0800521 } finally {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900522 if (!success) {
523 if (dstFile != null) {
524 try {
525 dstFile.closeWithError("Error copying bytes.");
526 } catch (IOException closeError) {
527 Log.w(TAG, "Error closing destination.", closeError);
528 }
529 }
530
531 if (DEBUG) Log.d(TAG, "Cleaning up failed operation leftovers.");
532 canceller.cancel();
533 try {
Tomasz Mikolajewskidb875432016-02-23 15:12:54 +0900534 deleteDocument(dest, destParent);
535 } catch (ResourceException e) {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900536 Log.w(TAG, "Failed to cleanup after copy error: " + src.derivedUri, e);
537 }
538 }
539
Steve McKay14e827a2016-01-06 18:32:13 -0800540 // This also ensures the file descriptors are closed.
Steve McKay35645432016-01-20 15:09:35 -0800541 IoUtils.closeQuietly(in);
542 IoUtils.closeQuietly(out);
Steve McKay14e827a2016-01-06 18:32:13 -0800543 }
Steve McKay14e827a2016-01-06 18:32:13 -0800544 }
545
546 /**
547 * Calculates the cumulative size of all the documents in the list. Directories are recursed
548 * into and totaled up.
549 *
550 * @param srcs
551 * @return Size in bytes.
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900552 * @throws ResourceException
Steve McKay14e827a2016-01-06 18:32:13 -0800553 */
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900554 private long calculateSize(List<DocumentInfo> srcs) throws ResourceException {
Steve McKay14e827a2016-01-06 18:32:13 -0800555 long result = 0;
556
557 for (DocumentInfo src : srcs) {
558 if (src.isDirectory()) {
559 // Directories need to be recursed into.
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900560 try {
561 result += calculateFileSizesRecursively(getClient(src), src.derivedUri);
562 } catch (RemoteException e) {
563 throw new ResourceException("Failed to obtain the client for %s.",
564 src.derivedUri);
565 }
Steve McKay14e827a2016-01-06 18:32:13 -0800566 } else {
567 result += src.size;
568 }
569 }
570 return result;
571 }
572
573 /**
574 * Calculates (recursively) the cumulative size of all the files under the given directory.
575 *
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900576 * @throws ResourceException
Steve McKay14e827a2016-01-06 18:32:13 -0800577 */
578 private static long calculateFileSizesRecursively(
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900579 ContentProviderClient client, Uri uri) throws ResourceException {
Steve McKay14e827a2016-01-06 18:32:13 -0800580 final String authority = uri.getAuthority();
Steve McKayecbf3c52016-01-13 17:17:39 -0800581 final Uri queryUri = buildChildDocumentsUri(authority, getDocumentId(uri));
Steve McKay14e827a2016-01-06 18:32:13 -0800582 final String queryColumns[] = new String[] {
583 Document.COLUMN_DOCUMENT_ID,
584 Document.COLUMN_MIME_TYPE,
585 Document.COLUMN_SIZE
586 };
587
588 long result = 0;
589 Cursor cursor = null;
590 try {
591 cursor = client.query(queryUri, queryColumns, null, null, null);
592 while (cursor.moveToNext()) {
593 if (Document.MIME_TYPE_DIR.equals(
594 getCursorString(cursor, Document.COLUMN_MIME_TYPE))) {
595 // Recurse into directories.
Steve McKayecbf3c52016-01-13 17:17:39 -0800596 final Uri dirUri = buildDocumentUri(authority,
Steve McKay14e827a2016-01-06 18:32:13 -0800597 getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
598 result += calculateFileSizesRecursively(client, dirUri);
599 } else {
600 // This may return -1 if the size isn't defined. Ignore those cases.
601 long size = getCursorLong(cursor, Document.COLUMN_SIZE);
602 result += size > 0 ? size : 0;
603 }
604 }
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900605 } catch (RemoteException | RuntimeException e) {
606 throw new ResourceException(
607 "Failed to calculate size for %s due to an exception.", uri, e);
Steve McKay14e827a2016-01-06 18:32:13 -0800608 } finally {
609 IoUtils.closeQuietly(cursor);
610 }
611
612 return result;
613 }
614
Steve McKay14e827a2016-01-06 18:32:13 -0800615 /**
616 * Returns true if {@code doc} is a descendant of {@code parentDoc}.
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900617 * @throws ResourceException
Steve McKay14e827a2016-01-06 18:32:13 -0800618 */
Steve McKay35645432016-01-20 15:09:35 -0800619 boolean isDescendentOf(DocumentInfo doc, DocumentInfo parent)
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900620 throws ResourceException {
Steve McKay35645432016-01-20 15:09:35 -0800621 if (parent.isDirectory() && doc.authority.equals(parent.authority)) {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900622 try {
623 return isChildDocument(getClient(doc), doc.derivedUri, parent.derivedUri);
624 } catch (RemoteException | RuntimeException e) {
625 throw new ResourceException(
626 "Failed to check if %s is a child of %s due to an exception.",
627 doc.derivedUri, parent.derivedUri, e);
628 }
Steve McKay14e827a2016-01-06 18:32:13 -0800629 }
630 return false;
631 }
Steve McKayecbf3c52016-01-13 17:17:39 -0800632
Steve McKay35645432016-01-20 15:09:35 -0800633 @Override
634 public String toString() {
635 return new StringBuilder()
636 .append("CopyJob")
637 .append("{")
638 .append("id=" + id)
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900639 .append(", srcs=" + mSrcs)
Steve McKay35645432016-01-20 15:09:35 -0800640 .append(", destination=" + stack)
641 .append("}")
642 .toString();
643 }
Steve McKayecbf3c52016-01-13 17:17:39 -0800644}