blob: 844d07ad1721ef674c9c6a99d0bed249683d7f63 [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) {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900132 if (mBatchSize >= 0) {
133 double completed = (double) this.mBytesCopied / mBatchSize;
134 mProgressBuilder.setProgress(100, (int) (completed * 100), false);
135 mProgressBuilder.setContentInfo(
136 NumberFormat.getPercentInstance().format(completed));
137 } else {
138 // If the total file size failed to compute on some files, then show
139 // an indeterminate spinner. CopyJob would most likely fail on those
140 // files while copying, but would continue with another files.
141 // Also, if the total size is 0 bytes, show an indeterminate spinner.
142 mProgressBuilder.setProgress(0, 0, true);
143 }
144
Steve McKay14e827a2016-01-06 18:32:13 -0800145 if (mRemainingTime > 0) {
Steve McKayecbf3c52016-01-13 17:17:39 -0800146 mProgressBuilder.setContentText(service.getString(msgId,
Steve McKay14e827a2016-01-06 18:32:13 -0800147 DateUtils.formatDuration(mRemainingTime)));
148 } else {
149 mProgressBuilder.setContentText(null);
150 }
151
152 // Remember when we last returned progress so we can provide an answer
153 // in shouldUpdateProgress.
154 mLastNotificationTime = elapsedRealtime();
155 return mProgressBuilder.build();
156 }
157
158 public Notification getProgressNotification() {
159 return getProgressNotification(R.string.copy_remaining);
160 }
161
162 void onBytesCopied(long numBytes) {
163 this.mBytesCopied += numBytes;
164 }
165
166 /**
167 * Generates an estimate of the remaining time in the copy.
168 */
169 void updateRemainingTimeEstimate() {
170 long elapsedTime = elapsedRealtime() - mStartTime;
171
172 final long sampleDuration = elapsedTime - mSampleTime;
173 final long sampleSpeed = ((mBytesCopied - mBytesCopiedSample) * 1000) / sampleDuration;
174 if (mSpeed == 0) {
175 mSpeed = sampleSpeed;
176 } else {
177 mSpeed = ((3 * mSpeed) + sampleSpeed) / 4;
178 }
179
180 if (mSampleTime > 0 && mSpeed > 0) {
181 mRemainingTime = ((mBatchSize - mBytesCopied) * 1000) / mSpeed;
182 } else {
183 mRemainingTime = 0;
184 }
185
186 mSampleTime = elapsedTime;
187 mBytesCopiedSample = mBytesCopied;
188 }
189
190 @Override
191 Notification getFailureNotification() {
192 return getFailureNotification(
193 R.plurals.copy_error_notification_title, R.drawable.ic_menu_copy);
194 }
195
196 @Override
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +0900197 Notification getWarningNotification() {
Tomasz Mikolajewskicd270152016-02-01 12:01:14 +0900198 final Intent navigateIntent = buildNavigateIntent(INTENT_TAG_WARNING);
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +0900199 navigateIntent.putExtra(EXTRA_DIALOG_TYPE, DIALOG_TYPE_CONVERTED);
200 navigateIntent.putExtra(EXTRA_OPERATION, operationType);
201
202 navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, convertedFiles);
203
204 // TODO: Consider adding a dialog on tapping the notification with a list of
205 // converted files.
206 final Notification.Builder warningBuilder = new Notification.Builder(service)
207 .setContentTitle(service.getResources().getString(
208 R.string.notification_copy_files_converted_title))
209 .setContentText(service.getString(
210 R.string.notification_touch_for_details))
211 .setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent,
212 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
213 .setCategory(Notification.CATEGORY_ERROR)
214 .setSmallIcon(R.drawable.ic_menu_copy)
215 .setAutoCancel(true);
216 return warningBuilder.build();
217 }
218
219 @Override
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900220 void start() {
Steve McKay14e827a2016-01-06 18:32:13 -0800221 mStartTime = elapsedRealtime();
222
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900223 try {
224 mBatchSize = calculateSize(mSrcs);
225 } catch (ResourceException e) {
226 Log.w(TAG, "Failed to calculate total size. Copying without progress.");
227 mBatchSize = -1;
228 }
Steve McKay14e827a2016-01-06 18:32:13 -0800229
230 DocumentInfo srcInfo;
Ben Kwad5b2af12016-01-28 16:39:57 -0800231 DocumentInfo dstInfo = stack.peek();
Steve McKay35645432016-01-20 15:09:35 -0800232 for (int i = 0; i < mSrcs.size() && !isCanceled(); ++i) {
233 srcInfo = mSrcs.get(i);
Steve McKay14e827a2016-01-06 18:32:13 -0800234
235 // Guard unsupported recursive operation.
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900236 try {
237 if (dstInfo.equals(srcInfo) || isDescendentOf(srcInfo, dstInfo)) {
238 throw new ResourceException("Cannot copy to itself recursively.");
239 }
240 } catch (ResourceException e) {
241 Log.e(TAG, e.toString());
242 onFileFailed(srcInfo);
Steve McKay14e827a2016-01-06 18:32:13 -0800243 continue;
244 }
245
246 if (DEBUG) Log.d(TAG,
Steve McKayecbf3c52016-01-13 17:17:39 -0800247 "Copying " + srcInfo.displayName + " (" + srcInfo.derivedUri + ")"
248 + " to " + dstInfo.displayName + " (" + dstInfo.derivedUri + ")");
Steve McKay14e827a2016-01-06 18:32:13 -0800249
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900250 try {
251 processDocument(srcInfo, null, dstInfo);
252 } catch (ResourceException e) {
253 Log.e(TAG, e.toString());
254 onFileFailed(srcInfo);
255 }
Steve McKay14e827a2016-01-06 18:32:13 -0800256 }
Ben Kwad5b2af12016-01-28 16:39:57 -0800257 Metrics.logFileOperation(service, operationType, mSrcs, dstInfo);
Steve McKay14e827a2016-01-06 18:32:13 -0800258 }
259
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +0900260 @Override
261 boolean hasWarnings() {
262 return !convertedFiles.isEmpty();
263 }
264
Steve McKay14e827a2016-01-06 18:32:13 -0800265 /**
266 * Logs progress on the current copy operation. Displays/Updates the progress notification.
267 *
268 * @param bytesCopied
269 */
270 private void makeCopyProgress(long bytesCopied) {
271 onBytesCopied(bytesCopied);
272 if (shouldUpdateProgress()) {
273 updateRemainingTimeEstimate();
274 listener.onProgress(this);
275 }
276 }
277
278 /**
279 * Copies a the given document to the given location.
280 *
Steve McKay35645432016-01-20 15:09:35 -0800281 * @param src DocumentInfos for the documents to copy.
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900282 * @param srcParent DocumentInfo for the parent of the document to process.
Steve McKay14e827a2016-01-06 18:32:13 -0800283 * @param dstDirInfo The destination directory.
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900284 * @throws ResourceException
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900285 *
286 * TODO: Stop passing srcParent, as it's not used for copy, but for move only.
Steve McKay14e827a2016-01-06 18:32:13 -0800287 */
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900288 void processDocument(DocumentInfo src, DocumentInfo srcParent,
289 DocumentInfo dstDirInfo) throws ResourceException {
Steve McKay14e827a2016-01-06 18:32:13 -0800290
291 // TODO: When optimized copy kicks in, we'll not making any progress updates.
292 // For now. Local storage isn't using optimized copy.
293
Tomasz Mikolajewski67048082016-01-21 10:00:33 +0900294 // When copying within the same provider, try to use optimized copying.
Steve McKay14e827a2016-01-06 18:32:13 -0800295 // If not supported, then fallback to byte-by-byte copy/move.
Steve McKay35645432016-01-20 15:09:35 -0800296 if (src.authority.equals(dstDirInfo.authority)) {
297 if ((src.flags & Document.FLAG_SUPPORTS_COPY) != 0) {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900298 try {
299 if (DocumentsContract.copyDocument(getClient(src), src.derivedUri,
300 dstDirInfo.derivedUri) == null) {
301 throw new ResourceException("Provider side copy failed for document %s.",
302 src.derivedUri);
303 }
304 } catch (ResourceException e) {
305 throw e;
306 } catch (RemoteException | RuntimeException e) {
307 throw new ResourceException(
308 "Provider side copy failed for document %s due to an exception.",
309 src.derivedUri, e);
Steve McKay14e827a2016-01-06 18:32:13 -0800310 }
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900311 return;
Steve McKay14e827a2016-01-06 18:32:13 -0800312 }
313 }
314
315 // If we couldn't do an optimized copy...we fall back to vanilla byte copy.
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900316 byteCopyDocument(src, dstDirInfo);
Steve McKay14e827a2016-01-06 18:32:13 -0800317 }
318
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900319 void byteCopyDocument(DocumentInfo src, DocumentInfo dest) throws ResourceException {
Steve McKay14e827a2016-01-06 18:32:13 -0800320 final String dstMimeType;
321 final String dstDisplayName;
322
Steve McKay35645432016-01-20 15:09:35 -0800323 if (DEBUG) Log.d(TAG, "Doing byte copy of document: " + src);
Steve McKay14e827a2016-01-06 18:32:13 -0800324 // If the file is virtual, but can be converted to another format, then try to copy it
325 // as such format. Also, append an extension for the target mime type (if known).
Steve McKay35645432016-01-20 15:09:35 -0800326 if (src.isVirtualDocument()) {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900327 String[] streamTypes = null;
328 try {
329 streamTypes = getContentResolver().getStreamTypes(src.derivedUri, "*/*");
330 } catch (RuntimeException e) {
331 throw new ResourceException(
332 "Failed to obtain streamable types for %s due to an exception.",
333 src.derivedUri, e);
334 }
Steve McKay14e827a2016-01-06 18:32:13 -0800335 if (streamTypes != null && streamTypes.length > 0) {
336 dstMimeType = streamTypes[0];
337 final String extension = MimeTypeMap.getSingleton().
338 getExtensionFromMimeType(dstMimeType);
Steve McKay35645432016-01-20 15:09:35 -0800339 dstDisplayName = src.displayName +
340 (extension != null ? "." + extension : src.displayName);
Steve McKay14e827a2016-01-06 18:32:13 -0800341 } else {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900342 throw new ResourceException("Cannot copy virtual file %s. No streamable formats "
343 + "available.", src.derivedUri);
Steve McKay14e827a2016-01-06 18:32:13 -0800344 }
345 } else {
Steve McKay35645432016-01-20 15:09:35 -0800346 dstMimeType = src.mimeType;
347 dstDisplayName = src.displayName;
Steve McKay14e827a2016-01-06 18:32:13 -0800348 }
349
350 // Create the target document (either a file or a directory), then copy recursively the
351 // contents (bytes or children).
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900352 Uri dstUri = null;
353 try {
354 dstUri = DocumentsContract.createDocument(
355 getClient(dest), dest.derivedUri, dstMimeType, dstDisplayName);
356 } catch (RemoteException | RuntimeException e) {
357 throw new ResourceException(
358 "Couldn't create destination document " + dstDisplayName + " in directory %s "
359 + "due to an exception.", dest.derivedUri, e);
360 }
Steve McKay14e827a2016-01-06 18:32:13 -0800361 if (dstUri == null) {
362 // If this is a directory, the entire subdir will not be copied over.
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900363 throw new ResourceException(
364 "Couldn't create destination document " + dstDisplayName + " in directory %s.",
365 dest.derivedUri);
Steve McKay14e827a2016-01-06 18:32:13 -0800366 }
367
368 DocumentInfo dstInfo = null;
369 try {
370 dstInfo = DocumentInfo.fromUri(getContentResolver(), dstUri);
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900371 } catch (FileNotFoundException | RuntimeException e) {
372 throw new ResourceException("Could not load DocumentInfo for newly created file %s.",
373 dstUri);
Steve McKay14e827a2016-01-06 18:32:13 -0800374 }
375
Steve McKay35645432016-01-20 15:09:35 -0800376 if (Document.MIME_TYPE_DIR.equals(src.mimeType)) {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900377 copyDirectoryHelper(src, dstInfo);
Steve McKay14e827a2016-01-06 18:32:13 -0800378 } else {
Tomasz Mikolajewskidb875432016-02-23 15:12:54 +0900379 copyFileHelper(src, dstInfo, dest, dstMimeType);
Steve McKay14e827a2016-01-06 18:32:13 -0800380 }
Steve McKay14e827a2016-01-06 18:32:13 -0800381 }
382
383 /**
384 * Handles recursion into a directory and copying its contents. Note that in linux terms, this
385 * does the equivalent of "cp src/* dst", not "cp -r src dst".
386 *
Steve McKay35645432016-01-20 15:09:35 -0800387 * @param srcDir Info of the directory to copy from. The routine will copy the directory's
Steve McKay14e827a2016-01-06 18:32:13 -0800388 * contents, not the directory itself.
Steve McKay35645432016-01-20 15:09:35 -0800389 * @param destDir Info of the directory to copy to. Must be created beforehand.
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900390 * @throws ResourceException
Steve McKay14e827a2016-01-06 18:32:13 -0800391 */
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900392 private void copyDirectoryHelper(DocumentInfo srcDir, DocumentInfo destDir)
393 throws ResourceException {
Steve McKay14e827a2016-01-06 18:32:13 -0800394 // Recurse into directories. Copy children into the new subdirectory.
395 final String queryColumns[] = new String[] {
396 Document.COLUMN_DISPLAY_NAME,
397 Document.COLUMN_DOCUMENT_ID,
398 Document.COLUMN_MIME_TYPE,
399 Document.COLUMN_SIZE,
400 Document.COLUMN_FLAGS
401 };
402 Cursor cursor = null;
403 boolean success = true;
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900404 // Iterate over srcs in the directory; copy to the destination directory.
405 final Uri queryUri = buildChildDocumentsUri(srcDir.authority, srcDir.documentId);
Steve McKay14e827a2016-01-06 18:32:13 -0800406 try {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900407 try {
408 cursor = getClient(srcDir).query(queryUri, queryColumns, null, null, null);
409 } catch (RemoteException | RuntimeException e) {
410 throw new ResourceException("Failed to query children of %s due to an exception.",
411 srcDir.derivedUri, e);
Steve McKay14e827a2016-01-06 18:32:13 -0800412 }
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900413
414 DocumentInfo src;
415 while (cursor.moveToNext() && !isCanceled()) {
416 try {
417 src = DocumentInfo.fromCursor(cursor, srcDir.authority);
418 processDocument(src, srcDir, destDir);
419 } catch (RuntimeException e) {
420 Log.e(TAG, "Failed to recursively process a file %s due to an exception."
421 .format(srcDir.derivedUri.toString()), e);
422 success = false;
423 }
424 }
425 } catch (RuntimeException e) {
426 Log.e(TAG, "Failed to copy a file %s to %s. "
427 .format(srcDir.derivedUri.toString(), destDir.derivedUri.toString()), e);
428 success = false;
Steve McKay14e827a2016-01-06 18:32:13 -0800429 } finally {
430 IoUtils.closeQuietly(cursor);
431 }
432
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900433 if (!success) {
434 throw new RuntimeException("Some files failed to copy during a recursive "
435 + "directory copy.");
436 }
Steve McKay14e827a2016-01-06 18:32:13 -0800437 }
438
439 /**
440 * Handles copying a single file.
441 *
Tomasz Mikolajewskidb875432016-02-23 15:12:54 +0900442 * @param src Info of the file to copy from.
443 * @param dest Info of the *file* to copy to. Must be created beforehand.
444 * @param destParent Info of the parent of the destination.
Steve McKay14e827a2016-01-06 18:32:13 -0800445 * @param mimeType Mime type for the target. Can be different than source for virtual files.
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900446 * @throws ResourceException
Steve McKay14e827a2016-01-06 18:32:13 -0800447 */
Tomasz Mikolajewskidb875432016-02-23 15:12:54 +0900448 private void copyFileHelper(DocumentInfo src, DocumentInfo dest, DocumentInfo destParent,
449 String mimeType) throws ResourceException {
Steve McKay14e827a2016-01-06 18:32:13 -0800450 CancellationSignal canceller = new CancellationSignal();
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900451 AssetFileDescriptor srcFileAsAsset = null;
Steve McKay14e827a2016-01-06 18:32:13 -0800452 ParcelFileDescriptor srcFile = null;
453 ParcelFileDescriptor dstFile = null;
Steve McKay35645432016-01-20 15:09:35 -0800454 InputStream in = null;
455 OutputStream out = null;
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900456 boolean success = false;
Steve McKay14e827a2016-01-06 18:32:13 -0800457
Steve McKay14e827a2016-01-06 18:32:13 -0800458 try {
459 // If the file is virtual, but can be converted to another format, then try to copy it
460 // as such format.
Steve McKay35645432016-01-20 15:09:35 -0800461 if (src.isVirtualDocument()) {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900462 try {
463 srcFileAsAsset = getClient(src).openTypedAssetFileDescriptor(
Steve McKay35645432016-01-20 15:09:35 -0800464 src.derivedUri, mimeType, null, canceller);
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900465 } catch (FileNotFoundException | RemoteException | RuntimeException e) {
466 throw new ResourceException("Failed to open a file as asset for %s due to an "
467 + "exception.", src.derivedUri, e);
468 }
Steve McKay14e827a2016-01-06 18:32:13 -0800469 srcFile = srcFileAsAsset.getParcelFileDescriptor();
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900470 try {
471 in = new AssetFileDescriptor.AutoCloseInputStream(srcFileAsAsset);
472 } catch (IOException e) {
473 throw new ResourceException("Failed to open a file input stream for %s due "
474 + "an exception.", src.derivedUri, e);
475 }
Steve McKay14e827a2016-01-06 18:32:13 -0800476 } else {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900477 try {
478 srcFile = getClient(src).openFile(src.derivedUri, "r", canceller);
479 } catch (FileNotFoundException | RemoteException | RuntimeException e) {
480 throw new ResourceException(
481 "Failed to open a file for %s due to an exception.", src.derivedUri, e);
482 }
Steve McKay35645432016-01-20 15:09:35 -0800483 in = new ParcelFileDescriptor.AutoCloseInputStream(srcFile);
Steve McKay14e827a2016-01-06 18:32:13 -0800484 }
485
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900486 try {
487 dstFile = getClient(dest).openFile(dest.derivedUri, "w", canceller);
488 } catch (FileNotFoundException | RemoteException | RuntimeException e) {
489 throw new ResourceException("Failed to open the destination file %s for writing "
490 + "due to an exception.", dest.derivedUri, e);
491 }
Steve McKay35645432016-01-20 15:09:35 -0800492 out = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);
Steve McKay14e827a2016-01-06 18:32:13 -0800493
Steve McKayecbf3c52016-01-13 17:17:39 -0800494 byte[] buffer = new byte[32 * 1024];
Steve McKay14e827a2016-01-06 18:32:13 -0800495 int len;
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900496 try {
497 while ((len = in.read(buffer)) != -1) {
498 if (isCanceled()) {
499 throw new ResourceException("Canceled copy mid-copy of %s",
500 src.derivedUri);
501 }
502 out.write(buffer, 0, len);
503 makeCopyProgress(len);
Steve McKay14e827a2016-01-06 18:32:13 -0800504 }
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900505
506 srcFile.checkError();
507 } catch (IOException e) {
508 throw new ResourceException(
509 "Failed to copy bytes from %s to %s due to an IO exception.",
510 src.derivedUri, dest.derivedUri, e);
Steve McKay14e827a2016-01-06 18:32:13 -0800511 }
512
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900513 if (src.isVirtualDocument()) {
514 convertedFiles.add(src);
Steve McKay14e827a2016-01-06 18:32:13 -0800515 }
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900516
517 success = true;
Steve McKay14e827a2016-01-06 18:32:13 -0800518 } finally {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900519 if (!success) {
520 if (dstFile != null) {
521 try {
522 dstFile.closeWithError("Error copying bytes.");
523 } catch (IOException closeError) {
524 Log.w(TAG, "Error closing destination.", closeError);
525 }
526 }
527
528 if (DEBUG) Log.d(TAG, "Cleaning up failed operation leftovers.");
529 canceller.cancel();
530 try {
Tomasz Mikolajewskidb875432016-02-23 15:12:54 +0900531 deleteDocument(dest, destParent);
532 } catch (ResourceException e) {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900533 Log.w(TAG, "Failed to cleanup after copy error: " + src.derivedUri, e);
534 }
535 }
536
Steve McKay14e827a2016-01-06 18:32:13 -0800537 // This also ensures the file descriptors are closed.
Steve McKay35645432016-01-20 15:09:35 -0800538 IoUtils.closeQuietly(in);
539 IoUtils.closeQuietly(out);
Steve McKay14e827a2016-01-06 18:32:13 -0800540 }
Steve McKay14e827a2016-01-06 18:32:13 -0800541 }
542
543 /**
544 * Calculates the cumulative size of all the documents in the list. Directories are recursed
545 * into and totaled up.
546 *
547 * @param srcs
548 * @return Size in bytes.
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900549 * @throws ResourceException
Steve McKay14e827a2016-01-06 18:32:13 -0800550 */
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900551 private long calculateSize(List<DocumentInfo> srcs) throws ResourceException {
Steve McKay14e827a2016-01-06 18:32:13 -0800552 long result = 0;
553
554 for (DocumentInfo src : srcs) {
555 if (src.isDirectory()) {
556 // Directories need to be recursed into.
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900557 try {
558 result += calculateFileSizesRecursively(getClient(src), src.derivedUri);
559 } catch (RemoteException e) {
560 throw new ResourceException("Failed to obtain the client for %s.",
561 src.derivedUri);
562 }
Steve McKay14e827a2016-01-06 18:32:13 -0800563 } else {
564 result += src.size;
565 }
566 }
567 return result;
568 }
569
570 /**
571 * Calculates (recursively) the cumulative size of all the files under the given directory.
572 *
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900573 * @throws ResourceException
Steve McKay14e827a2016-01-06 18:32:13 -0800574 */
575 private static long calculateFileSizesRecursively(
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900576 ContentProviderClient client, Uri uri) throws ResourceException {
Steve McKay14e827a2016-01-06 18:32:13 -0800577 final String authority = uri.getAuthority();
Steve McKayecbf3c52016-01-13 17:17:39 -0800578 final Uri queryUri = buildChildDocumentsUri(authority, getDocumentId(uri));
Steve McKay14e827a2016-01-06 18:32:13 -0800579 final String queryColumns[] = new String[] {
580 Document.COLUMN_DOCUMENT_ID,
581 Document.COLUMN_MIME_TYPE,
582 Document.COLUMN_SIZE
583 };
584
585 long result = 0;
586 Cursor cursor = null;
587 try {
588 cursor = client.query(queryUri, queryColumns, null, null, null);
589 while (cursor.moveToNext()) {
590 if (Document.MIME_TYPE_DIR.equals(
591 getCursorString(cursor, Document.COLUMN_MIME_TYPE))) {
592 // Recurse into directories.
Steve McKayecbf3c52016-01-13 17:17:39 -0800593 final Uri dirUri = buildDocumentUri(authority,
Steve McKay14e827a2016-01-06 18:32:13 -0800594 getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
595 result += calculateFileSizesRecursively(client, dirUri);
596 } else {
597 // This may return -1 if the size isn't defined. Ignore those cases.
598 long size = getCursorLong(cursor, Document.COLUMN_SIZE);
599 result += size > 0 ? size : 0;
600 }
601 }
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900602 } catch (RemoteException | RuntimeException e) {
603 throw new ResourceException(
604 "Failed to calculate size for %s due to an exception.", uri, e);
Steve McKay14e827a2016-01-06 18:32:13 -0800605 } finally {
606 IoUtils.closeQuietly(cursor);
607 }
608
609 return result;
610 }
611
Steve McKay14e827a2016-01-06 18:32:13 -0800612 /**
613 * Returns true if {@code doc} is a descendant of {@code parentDoc}.
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900614 * @throws ResourceException
Steve McKay14e827a2016-01-06 18:32:13 -0800615 */
Steve McKay35645432016-01-20 15:09:35 -0800616 boolean isDescendentOf(DocumentInfo doc, DocumentInfo parent)
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900617 throws ResourceException {
Steve McKay35645432016-01-20 15:09:35 -0800618 if (parent.isDirectory() && doc.authority.equals(parent.authority)) {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900619 try {
620 return isChildDocument(getClient(doc), doc.derivedUri, parent.derivedUri);
621 } catch (RemoteException | RuntimeException e) {
622 throw new ResourceException(
623 "Failed to check if %s is a child of %s due to an exception.",
624 doc.derivedUri, parent.derivedUri, e);
625 }
Steve McKay14e827a2016-01-06 18:32:13 -0800626 }
627 return false;
628 }
Steve McKayecbf3c52016-01-13 17:17:39 -0800629
Steve McKay35645432016-01-20 15:09:35 -0800630 @Override
631 public String toString() {
632 return new StringBuilder()
633 .append("CopyJob")
634 .append("{")
635 .append("id=" + id)
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900636 .append(", srcs=" + mSrcs)
Steve McKay35645432016-01-20 15:09:35 -0800637 .append(", destination=" + stack)
638 .append("}")
639 .toString();
640 }
Steve McKayecbf3c52016-01-13 17:17:39 -0800641}