blob: 9ed2abf9c9b8e56fa6af86fc7527e67d3ca15b3b [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;
Steve McKayc83baa02016-01-06 18:32:13 -080032
33import android.annotation.StringRes;
34import android.app.Notification;
35import android.app.Notification.Builder;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090036import android.app.PendingIntent;
Steve McKayc83baa02016-01-06 18:32:13 -080037import android.content.ContentProviderClient;
38import android.content.Context;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090039import android.content.Intent;
Steve McKayc83baa02016-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;
48import android.text.format.DateUtils;
49import android.util.Log;
50import android.webkit.MimeTypeMap;
51
Ben Kwafaa27202016-01-28 16:39:57 -080052import com.android.documentsui.Metrics;
Steve McKayc83baa02016-01-06 18:32:13 -080053import 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 {
Steve McKay003097d2016-02-23 10:06:50 -080069
Steve McKayc83baa02016-01-06 18:32:13 -080070 private static final String TAG = "CopyJob";
Steve McKay003097d2016-02-23 10:06:50 -080071 private static final int PROGRESS_INTERVAL_MILLIS = 500;
72
Steve McKay97b4be42016-01-20 15:09:35 -080073 final List<DocumentInfo> mSrcs;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090074 final ArrayList<DocumentInfo> convertedFiles = new ArrayList<>();
Steve McKayc83baa02016-01-06 18:32:13 -080075
76 private long mStartTime = -1;
Steve McKay003097d2016-02-23 10:06:50 -080077
Steve McKayc83baa02016-01-06 18:32:13 -080078 private long mBatchSize;
79 private long mBytesCopied;
80 private long mLastNotificationTime;
81 // Speed estimation
82 private long mBytesCopiedSample;
83 private long mSampleTime;
84 private long mSpeed;
85 private long mRemainingTime;
86
87 /**
88 * Copies files to a destination identified by {@code destination}.
89 * @see @link {@link Job} constructor for most param descriptions.
90 *
91 * @param srcs List of files to be copied.
92 */
Steve McKaybbeba522016-01-13 17:17:39 -080093 CopyJob(Context service, Context appContext, Listener listener,
Steve McKay97b4be42016-01-20 15:09:35 -080094 String id, DocumentStack stack, List<DocumentInfo> srcs) {
95 super(service, appContext, listener, OPERATION_COPY, id, stack);
Steve McKaybbeba522016-01-13 17:17:39 -080096
Steve McKay0af8afd2016-02-25 13:34:03 -080097 assert(!srcs.isEmpty());
Steve McKay97b4be42016-01-20 15:09:35 -080098 this.mSrcs = srcs;
Steve McKaybbeba522016-01-13 17:17:39 -080099 }
100
101 /**
102 * @see @link {@link Job} constructor for most param descriptions.
103 *
104 * @param srcs List of files to be copied.
105 */
106 CopyJob(Context service, Context appContext, Listener listener,
107 @OpType int opType, String id, DocumentStack destination, List<DocumentInfo> srcs) {
108 super(service, appContext, listener, opType, id, destination);
Steve McKayc83baa02016-01-06 18:32:13 -0800109
Steve McKay0af8afd2016-02-25 13:34:03 -0800110 assert(!srcs.isEmpty());
Steve McKay97b4be42016-01-20 15:09:35 -0800111 this.mSrcs = srcs;
Steve McKayc83baa02016-01-06 18:32:13 -0800112 }
113
114 @Override
115 Builder createProgressBuilder() {
116 return super.createProgressBuilder(
Steve McKaybbeba522016-01-13 17:17:39 -0800117 service.getString(R.string.copy_notification_title),
Steve McKayc83baa02016-01-06 18:32:13 -0800118 R.drawable.ic_menu_copy,
Steve McKaybbeba522016-01-13 17:17:39 -0800119 service.getString(android.R.string.cancel),
Steve McKayc83baa02016-01-06 18:32:13 -0800120 R.drawable.ic_cab_cancel);
121 }
122
123 @Override
124 public Notification getSetupNotification() {
Steve McKaybbeba522016-01-13 17:17:39 -0800125 return getSetupNotification(service.getString(R.string.copy_preparing));
Steve McKayc83baa02016-01-06 18:32:13 -0800126 }
127
128 public boolean shouldUpdateProgress() {
129 // Wait a while between updates :)
130 return elapsedRealtime() - mLastNotificationTime > PROGRESS_INTERVAL_MILLIS;
131 }
132
133 Notification getProgressNotification(@StringRes int msgId) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900134 if (mBatchSize >= 0) {
135 double completed = (double) this.mBytesCopied / mBatchSize;
136 mProgressBuilder.setProgress(100, (int) (completed * 100), false);
137 mProgressBuilder.setContentInfo(
138 NumberFormat.getPercentInstance().format(completed));
139 } else {
140 // If the total file size failed to compute on some files, then show
141 // an indeterminate spinner. CopyJob would most likely fail on those
142 // files while copying, but would continue with another files.
143 // Also, if the total size is 0 bytes, show an indeterminate spinner.
144 mProgressBuilder.setProgress(0, 0, true);
145 }
146
Steve McKayc83baa02016-01-06 18:32:13 -0800147 if (mRemainingTime > 0) {
Steve McKaybbeba522016-01-13 17:17:39 -0800148 mProgressBuilder.setContentText(service.getString(msgId,
Steve McKayc83baa02016-01-06 18:32:13 -0800149 DateUtils.formatDuration(mRemainingTime)));
150 } else {
151 mProgressBuilder.setContentText(null);
152 }
153
154 // Remember when we last returned progress so we can provide an answer
155 // in shouldUpdateProgress.
156 mLastNotificationTime = elapsedRealtime();
157 return mProgressBuilder.build();
158 }
159
160 public Notification getProgressNotification() {
161 return getProgressNotification(R.string.copy_remaining);
162 }
163
164 void onBytesCopied(long numBytes) {
165 this.mBytesCopied += numBytes;
166 }
167
168 /**
169 * Generates an estimate of the remaining time in the copy.
170 */
171 void updateRemainingTimeEstimate() {
172 long elapsedTime = elapsedRealtime() - mStartTime;
173
174 final long sampleDuration = elapsedTime - mSampleTime;
175 final long sampleSpeed = ((mBytesCopied - mBytesCopiedSample) * 1000) / sampleDuration;
176 if (mSpeed == 0) {
177 mSpeed = sampleSpeed;
178 } else {
179 mSpeed = ((3 * mSpeed) + sampleSpeed) / 4;
180 }
181
182 if (mSampleTime > 0 && mSpeed > 0) {
183 mRemainingTime = ((mBatchSize - mBytesCopied) * 1000) / mSpeed;
184 } else {
185 mRemainingTime = 0;
186 }
187
188 mSampleTime = elapsedTime;
189 mBytesCopiedSample = mBytesCopied;
190 }
191
192 @Override
193 Notification getFailureNotification() {
194 return getFailureNotification(
195 R.plurals.copy_error_notification_title, R.drawable.ic_menu_copy);
196 }
197
198 @Override
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900199 Notification getWarningNotification() {
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +0900200 final Intent navigateIntent = buildNavigateIntent(INTENT_TAG_WARNING);
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900201 navigateIntent.putExtra(EXTRA_DIALOG_TYPE, DIALOG_TYPE_CONVERTED);
202 navigateIntent.putExtra(EXTRA_OPERATION, operationType);
203
204 navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, convertedFiles);
205
206 // TODO: Consider adding a dialog on tapping the notification with a list of
207 // converted files.
208 final Notification.Builder warningBuilder = new Notification.Builder(service)
209 .setContentTitle(service.getResources().getString(
210 R.string.notification_copy_files_converted_title))
211 .setContentText(service.getString(
212 R.string.notification_touch_for_details))
213 .setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent,
214 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
215 .setCategory(Notification.CATEGORY_ERROR)
216 .setSmallIcon(R.drawable.ic_menu_copy)
217 .setAutoCancel(true);
218 return warningBuilder.build();
219 }
220
221 @Override
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900222 void start() {
Steve McKayc83baa02016-01-06 18:32:13 -0800223 mStartTime = elapsedRealtime();
224
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900225 try {
226 mBatchSize = calculateSize(mSrcs);
227 } catch (ResourceException e) {
228 Log.w(TAG, "Failed to calculate total size. Copying without progress.");
229 mBatchSize = -1;
230 }
Steve McKayc83baa02016-01-06 18:32:13 -0800231
232 DocumentInfo srcInfo;
Ben Kwafaa27202016-01-28 16:39:57 -0800233 DocumentInfo dstInfo = stack.peek();
Steve McKay97b4be42016-01-20 15:09:35 -0800234 for (int i = 0; i < mSrcs.size() && !isCanceled(); ++i) {
235 srcInfo = mSrcs.get(i);
Steve McKayc83baa02016-01-06 18:32:13 -0800236
237 // Guard unsupported recursive operation.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900238 try {
239 if (dstInfo.equals(srcInfo) || isDescendentOf(srcInfo, dstInfo)) {
240 throw new ResourceException("Cannot copy to itself recursively.");
241 }
242 } catch (ResourceException e) {
243 Log.e(TAG, e.toString());
244 onFileFailed(srcInfo);
Steve McKayc83baa02016-01-06 18:32:13 -0800245 continue;
246 }
247
248 if (DEBUG) Log.d(TAG,
Steve McKaybbeba522016-01-13 17:17:39 -0800249 "Copying " + srcInfo.displayName + " (" + srcInfo.derivedUri + ")"
250 + " to " + dstInfo.displayName + " (" + dstInfo.derivedUri + ")");
Steve McKayc83baa02016-01-06 18:32:13 -0800251
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900252 try {
253 processDocument(srcInfo, null, dstInfo);
254 } catch (ResourceException e) {
255 Log.e(TAG, e.toString());
256 onFileFailed(srcInfo);
257 }
Steve McKayc83baa02016-01-06 18:32:13 -0800258 }
Ben Kwafaa27202016-01-28 16:39:57 -0800259 Metrics.logFileOperation(service, operationType, mSrcs, dstInfo);
Steve McKayc83baa02016-01-06 18:32:13 -0800260 }
261
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900262 @Override
263 boolean hasWarnings() {
264 return !convertedFiles.isEmpty();
265 }
266
Steve McKayc83baa02016-01-06 18:32:13 -0800267 /**
268 * Logs progress on the current copy operation. Displays/Updates the progress notification.
269 *
270 * @param bytesCopied
271 */
272 private void makeCopyProgress(long bytesCopied) {
273 onBytesCopied(bytesCopied);
274 if (shouldUpdateProgress()) {
275 updateRemainingTimeEstimate();
276 listener.onProgress(this);
277 }
278 }
279
280 /**
281 * Copies a the given document to the given location.
282 *
Steve McKay97b4be42016-01-20 15:09:35 -0800283 * @param src DocumentInfos for the documents to copy.
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +0900284 * @param srcParent DocumentInfo for the parent of the document to process.
Steve McKayc83baa02016-01-06 18:32:13 -0800285 * @param dstDirInfo The destination directory.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900286 * @throws ResourceException
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +0900287 *
288 * TODO: Stop passing srcParent, as it's not used for copy, but for move only.
Steve McKayc83baa02016-01-06 18:32:13 -0800289 */
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900290 void processDocument(DocumentInfo src, DocumentInfo srcParent,
291 DocumentInfo dstDirInfo) throws ResourceException {
Steve McKayc83baa02016-01-06 18:32:13 -0800292
293 // TODO: When optimized copy kicks in, we'll not making any progress updates.
294 // For now. Local storage isn't using optimized copy.
295
Tomasz Mikolajewski4aa89672016-01-21 10:00:33 +0900296 // When copying within the same provider, try to use optimized copying.
Steve McKayc83baa02016-01-06 18:32:13 -0800297 // If not supported, then fallback to byte-by-byte copy/move.
Steve McKay97b4be42016-01-20 15:09:35 -0800298 if (src.authority.equals(dstDirInfo.authority)) {
299 if ((src.flags & Document.FLAG_SUPPORTS_COPY) != 0) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900300 try {
301 if (DocumentsContract.copyDocument(getClient(src), src.derivedUri,
Tomasz Mikolajewskife7f5362016-03-02 17:41:40 +0900302 dstDirInfo.derivedUri) != null) {
303 return;
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900304 }
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900305 } catch (RemoteException | RuntimeException e) {
Tomasz Mikolajewskife7f5362016-03-02 17:41:40 +0900306 Log.e(TAG, "Provider side copy failed for: " + src.derivedUri
307 + " due to an exception: " + e);
Steve McKayc83baa02016-01-06 18:32:13 -0800308 }
Tomasz Mikolajewskife7f5362016-03-02 17:41:40 +0900309 // If optimized copy fails, then fallback to byte-by-byte copy.
310 if (DEBUG) Log.d(TAG, "Fallback to byte-by-byte copy for: " + src.derivedUri);
Steve McKayc83baa02016-01-06 18:32:13 -0800311 }
312 }
313
314 // If we couldn't do an optimized copy...we fall back to vanilla byte copy.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900315 byteCopyDocument(src, dstDirInfo);
Steve McKayc83baa02016-01-06 18:32:13 -0800316 }
317
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900318 void byteCopyDocument(DocumentInfo src, DocumentInfo dest) throws ResourceException {
Steve McKayc83baa02016-01-06 18:32:13 -0800319 final String dstMimeType;
320 final String dstDisplayName;
321
Steve McKay97b4be42016-01-20 15:09:35 -0800322 if (DEBUG) Log.d(TAG, "Doing byte copy of document: " + src);
Steve McKayc83baa02016-01-06 18:32:13 -0800323 // If the file is virtual, but can be converted to another format, then try to copy it
324 // as such format. Also, append an extension for the target mime type (if known).
Steve McKay97b4be42016-01-20 15:09:35 -0800325 if (src.isVirtualDocument()) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900326 String[] streamTypes = null;
327 try {
328 streamTypes = getContentResolver().getStreamTypes(src.derivedUri, "*/*");
329 } catch (RuntimeException e) {
330 throw new ResourceException(
331 "Failed to obtain streamable types for %s due to an exception.",
332 src.derivedUri, e);
333 }
Steve McKayc83baa02016-01-06 18:32:13 -0800334 if (streamTypes != null && streamTypes.length > 0) {
335 dstMimeType = streamTypes[0];
336 final String extension = MimeTypeMap.getSingleton().
337 getExtensionFromMimeType(dstMimeType);
Steve McKay97b4be42016-01-20 15:09:35 -0800338 dstDisplayName = src.displayName +
339 (extension != null ? "." + extension : src.displayName);
Steve McKayc83baa02016-01-06 18:32:13 -0800340 } else {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900341 throw new ResourceException("Cannot copy virtual file %s. No streamable formats "
342 + "available.", src.derivedUri);
Steve McKayc83baa02016-01-06 18:32:13 -0800343 }
344 } else {
Steve McKay97b4be42016-01-20 15:09:35 -0800345 dstMimeType = src.mimeType;
346 dstDisplayName = src.displayName;
Steve McKayc83baa02016-01-06 18:32:13 -0800347 }
348
349 // Create the target document (either a file or a directory), then copy recursively the
350 // contents (bytes or children).
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900351 Uri dstUri = null;
352 try {
353 dstUri = DocumentsContract.createDocument(
354 getClient(dest), dest.derivedUri, dstMimeType, dstDisplayName);
355 } catch (RemoteException | RuntimeException e) {
356 throw new ResourceException(
357 "Couldn't create destination document " + dstDisplayName + " in directory %s "
358 + "due to an exception.", dest.derivedUri, e);
359 }
Steve McKayc83baa02016-01-06 18:32:13 -0800360 if (dstUri == null) {
361 // If this is a directory, the entire subdir will not be copied over.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900362 throw new ResourceException(
363 "Couldn't create destination document " + dstDisplayName + " in directory %s.",
364 dest.derivedUri);
Steve McKayc83baa02016-01-06 18:32:13 -0800365 }
366
367 DocumentInfo dstInfo = null;
368 try {
369 dstInfo = DocumentInfo.fromUri(getContentResolver(), dstUri);
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900370 } catch (FileNotFoundException | RuntimeException e) {
371 throw new ResourceException("Could not load DocumentInfo for newly created file %s.",
372 dstUri);
Steve McKayc83baa02016-01-06 18:32:13 -0800373 }
374
Steve McKay97b4be42016-01-20 15:09:35 -0800375 if (Document.MIME_TYPE_DIR.equals(src.mimeType)) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900376 copyDirectoryHelper(src, dstInfo);
Steve McKayc83baa02016-01-06 18:32:13 -0800377 } else {
Tomasz Mikolajewski8fe54982016-02-23 15:12:54 +0900378 copyFileHelper(src, dstInfo, dest, dstMimeType);
Steve McKayc83baa02016-01-06 18:32:13 -0800379 }
Steve McKayc83baa02016-01-06 18:32:13 -0800380 }
381
382 /**
383 * Handles recursion into a directory and copying its contents. Note that in linux terms, this
384 * does the equivalent of "cp src/* dst", not "cp -r src dst".
385 *
Steve McKay97b4be42016-01-20 15:09:35 -0800386 * @param srcDir Info of the directory to copy from. The routine will copy the directory's
Steve McKayc83baa02016-01-06 18:32:13 -0800387 * contents, not the directory itself.
Steve McKay97b4be42016-01-20 15:09:35 -0800388 * @param destDir Info of the directory to copy to. Must be created beforehand.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900389 * @throws ResourceException
Steve McKayc83baa02016-01-06 18:32:13 -0800390 */
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900391 private void copyDirectoryHelper(DocumentInfo srcDir, DocumentInfo destDir)
392 throws ResourceException {
Steve McKayc83baa02016-01-06 18:32:13 -0800393 // Recurse into directories. Copy children into the new subdirectory.
394 final String queryColumns[] = new String[] {
395 Document.COLUMN_DISPLAY_NAME,
396 Document.COLUMN_DOCUMENT_ID,
397 Document.COLUMN_MIME_TYPE,
398 Document.COLUMN_SIZE,
399 Document.COLUMN_FLAGS
400 };
401 Cursor cursor = null;
402 boolean success = true;
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900403 // Iterate over srcs in the directory; copy to the destination directory.
404 final Uri queryUri = buildChildDocumentsUri(srcDir.authority, srcDir.documentId);
Steve McKayc83baa02016-01-06 18:32:13 -0800405 try {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900406 try {
407 cursor = getClient(srcDir).query(queryUri, queryColumns, null, null, null);
408 } catch (RemoteException | RuntimeException e) {
409 throw new ResourceException("Failed to query children of %s due to an exception.",
410 srcDir.derivedUri, e);
Steve McKayc83baa02016-01-06 18:32:13 -0800411 }
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900412
413 DocumentInfo src;
414 while (cursor.moveToNext() && !isCanceled()) {
415 try {
416 src = DocumentInfo.fromCursor(cursor, srcDir.authority);
417 processDocument(src, srcDir, destDir);
418 } catch (RuntimeException e) {
419 Log.e(TAG, "Failed to recursively process a file %s due to an exception."
420 .format(srcDir.derivedUri.toString()), e);
421 success = false;
422 }
423 }
424 } catch (RuntimeException e) {
425 Log.e(TAG, "Failed to copy a file %s to %s. "
426 .format(srcDir.derivedUri.toString(), destDir.derivedUri.toString()), e);
427 success = false;
Steve McKayc83baa02016-01-06 18:32:13 -0800428 } finally {
429 IoUtils.closeQuietly(cursor);
430 }
431
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900432 if (!success) {
433 throw new RuntimeException("Some files failed to copy during a recursive "
434 + "directory copy.");
435 }
Steve McKayc83baa02016-01-06 18:32:13 -0800436 }
437
438 /**
439 * Handles copying a single file.
440 *
Tomasz Mikolajewski8fe54982016-02-23 15:12:54 +0900441 * @param src Info of the file to copy from.
442 * @param dest Info of the *file* to copy to. Must be created beforehand.
443 * @param destParent Info of the parent of the destination.
Steve McKayc83baa02016-01-06 18:32:13 -0800444 * @param mimeType Mime type for the target. Can be different than source for virtual files.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900445 * @throws ResourceException
Steve McKayc83baa02016-01-06 18:32:13 -0800446 */
Tomasz Mikolajewski8fe54982016-02-23 15:12:54 +0900447 private void copyFileHelper(DocumentInfo src, DocumentInfo dest, DocumentInfo destParent,
448 String mimeType) throws ResourceException {
Steve McKayc83baa02016-01-06 18:32:13 -0800449 CancellationSignal canceller = new CancellationSignal();
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900450 AssetFileDescriptor srcFileAsAsset = null;
Steve McKayc83baa02016-01-06 18:32:13 -0800451 ParcelFileDescriptor srcFile = null;
452 ParcelFileDescriptor dstFile = null;
Steve McKay97b4be42016-01-20 15:09:35 -0800453 InputStream in = null;
454 OutputStream out = null;
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900455 boolean success = false;
Steve McKayc83baa02016-01-06 18:32:13 -0800456
Steve McKayc83baa02016-01-06 18:32:13 -0800457 try {
458 // If the file is virtual, but can be converted to another format, then try to copy it
459 // as such format.
Steve McKay97b4be42016-01-20 15:09:35 -0800460 if (src.isVirtualDocument()) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900461 try {
462 srcFileAsAsset = getClient(src).openTypedAssetFileDescriptor(
Steve McKay97b4be42016-01-20 15:09:35 -0800463 src.derivedUri, mimeType, null, canceller);
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900464 } catch (FileNotFoundException | RemoteException | RuntimeException e) {
465 throw new ResourceException("Failed to open a file as asset for %s due to an "
466 + "exception.", src.derivedUri, e);
467 }
Steve McKayc83baa02016-01-06 18:32:13 -0800468 srcFile = srcFileAsAsset.getParcelFileDescriptor();
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900469 try {
470 in = new AssetFileDescriptor.AutoCloseInputStream(srcFileAsAsset);
471 } catch (IOException e) {
472 throw new ResourceException("Failed to open a file input stream for %s due "
473 + "an exception.", src.derivedUri, e);
474 }
Steve McKayc83baa02016-01-06 18:32:13 -0800475 } else {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900476 try {
477 srcFile = getClient(src).openFile(src.derivedUri, "r", canceller);
478 } catch (FileNotFoundException | RemoteException | RuntimeException e) {
479 throw new ResourceException(
480 "Failed to open a file for %s due to an exception.", src.derivedUri, e);
481 }
Steve McKay97b4be42016-01-20 15:09:35 -0800482 in = new ParcelFileDescriptor.AutoCloseInputStream(srcFile);
Steve McKayc83baa02016-01-06 18:32:13 -0800483 }
484
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900485 try {
486 dstFile = getClient(dest).openFile(dest.derivedUri, "w", canceller);
487 } catch (FileNotFoundException | RemoteException | RuntimeException e) {
488 throw new ResourceException("Failed to open the destination file %s for writing "
489 + "due to an exception.", dest.derivedUri, e);
490 }
Steve McKay97b4be42016-01-20 15:09:35 -0800491 out = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);
Steve McKayc83baa02016-01-06 18:32:13 -0800492
Steve McKaybbeba522016-01-13 17:17:39 -0800493 byte[] buffer = new byte[32 * 1024];
Steve McKayc83baa02016-01-06 18:32:13 -0800494 int len;
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900495 try {
496 while ((len = in.read(buffer)) != -1) {
497 if (isCanceled()) {
Steve McKay003097d2016-02-23 10:06:50 -0800498 if (DEBUG) Log.d(TAG, "Canceled copy mid-copy of: " + src.derivedUri);
499 return;
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900500 }
501 out.write(buffer, 0, len);
502 makeCopyProgress(len);
Steve McKayc83baa02016-01-06 18:32:13 -0800503 }
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900504
505 srcFile.checkError();
506 } catch (IOException e) {
507 throw new ResourceException(
508 "Failed to copy bytes from %s to %s due to an IO exception.",
509 src.derivedUri, dest.derivedUri, e);
Steve McKayc83baa02016-01-06 18:32:13 -0800510 }
511
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900512 if (src.isVirtualDocument()) {
513 convertedFiles.add(src);
Steve McKayc83baa02016-01-06 18:32:13 -0800514 }
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900515
516 success = true;
Steve McKayc83baa02016-01-06 18:32:13 -0800517 } finally {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900518 if (!success) {
519 if (dstFile != null) {
520 try {
521 dstFile.closeWithError("Error copying bytes.");
522 } catch (IOException closeError) {
523 Log.w(TAG, "Error closing destination.", closeError);
524 }
525 }
526
527 if (DEBUG) Log.d(TAG, "Cleaning up failed operation leftovers.");
528 canceller.cancel();
529 try {
Tomasz Mikolajewski8fe54982016-02-23 15:12:54 +0900530 deleteDocument(dest, destParent);
531 } catch (ResourceException e) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900532 Log.w(TAG, "Failed to cleanup after copy error: " + src.derivedUri, e);
533 }
534 }
535
Steve McKayc83baa02016-01-06 18:32:13 -0800536 // This also ensures the file descriptors are closed.
Steve McKay97b4be42016-01-20 15:09:35 -0800537 IoUtils.closeQuietly(in);
538 IoUtils.closeQuietly(out);
Steve McKayc83baa02016-01-06 18:32:13 -0800539 }
Steve McKayc83baa02016-01-06 18:32:13 -0800540 }
541
542 /**
543 * Calculates the cumulative size of all the documents in the list. Directories are recursed
544 * into and totaled up.
545 *
546 * @param srcs
547 * @return Size in bytes.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900548 * @throws ResourceException
Steve McKayc83baa02016-01-06 18:32:13 -0800549 */
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900550 private long calculateSize(List<DocumentInfo> srcs) throws ResourceException {
Steve McKayc83baa02016-01-06 18:32:13 -0800551 long result = 0;
552
553 for (DocumentInfo src : srcs) {
554 if (src.isDirectory()) {
555 // Directories need to be recursed into.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900556 try {
557 result += calculateFileSizesRecursively(getClient(src), src.derivedUri);
558 } catch (RemoteException e) {
559 throw new ResourceException("Failed to obtain the client for %s.",
560 src.derivedUri);
561 }
Steve McKayc83baa02016-01-06 18:32:13 -0800562 } else {
563 result += src.size;
564 }
565 }
566 return result;
567 }
568
569 /**
570 * Calculates (recursively) the cumulative size of all the files under the given directory.
571 *
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900572 * @throws ResourceException
Steve McKayc83baa02016-01-06 18:32:13 -0800573 */
574 private static long calculateFileSizesRecursively(
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900575 ContentProviderClient client, Uri uri) throws ResourceException {
Steve McKayc83baa02016-01-06 18:32:13 -0800576 final String authority = uri.getAuthority();
Steve McKaybbeba522016-01-13 17:17:39 -0800577 final Uri queryUri = buildChildDocumentsUri(authority, getDocumentId(uri));
Steve McKayc83baa02016-01-06 18:32:13 -0800578 final String queryColumns[] = new String[] {
579 Document.COLUMN_DOCUMENT_ID,
580 Document.COLUMN_MIME_TYPE,
581 Document.COLUMN_SIZE
582 };
583
584 long result = 0;
585 Cursor cursor = null;
586 try {
587 cursor = client.query(queryUri, queryColumns, null, null, null);
588 while (cursor.moveToNext()) {
589 if (Document.MIME_TYPE_DIR.equals(
590 getCursorString(cursor, Document.COLUMN_MIME_TYPE))) {
591 // Recurse into directories.
Steve McKaybbeba522016-01-13 17:17:39 -0800592 final Uri dirUri = buildDocumentUri(authority,
Steve McKayc83baa02016-01-06 18:32:13 -0800593 getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
594 result += calculateFileSizesRecursively(client, dirUri);
595 } else {
596 // This may return -1 if the size isn't defined. Ignore those cases.
597 long size = getCursorLong(cursor, Document.COLUMN_SIZE);
598 result += size > 0 ? size : 0;
599 }
600 }
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900601 } catch (RemoteException | RuntimeException e) {
602 throw new ResourceException(
603 "Failed to calculate size for %s due to an exception.", uri, e);
Steve McKayc83baa02016-01-06 18:32:13 -0800604 } finally {
605 IoUtils.closeQuietly(cursor);
606 }
607
608 return result;
609 }
610
Steve McKayc83baa02016-01-06 18:32:13 -0800611 /**
612 * Returns true if {@code doc} is a descendant of {@code parentDoc}.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900613 * @throws ResourceException
Steve McKayc83baa02016-01-06 18:32:13 -0800614 */
Steve McKay97b4be42016-01-20 15:09:35 -0800615 boolean isDescendentOf(DocumentInfo doc, DocumentInfo parent)
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900616 throws ResourceException {
Steve McKay97b4be42016-01-20 15:09:35 -0800617 if (parent.isDirectory() && doc.authority.equals(parent.authority)) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900618 try {
619 return isChildDocument(getClient(doc), doc.derivedUri, parent.derivedUri);
620 } catch (RemoteException | RuntimeException e) {
621 throw new ResourceException(
622 "Failed to check if %s is a child of %s due to an exception.",
623 doc.derivedUri, parent.derivedUri, e);
624 }
Steve McKayc83baa02016-01-06 18:32:13 -0800625 }
626 return false;
627 }
Steve McKaybbeba522016-01-13 17:17:39 -0800628
Steve McKay97b4be42016-01-20 15:09:35 -0800629 @Override
630 public String toString() {
631 return new StringBuilder()
632 .append("CopyJob")
633 .append("{")
634 .append("id=" + id)
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +0900635 .append(", srcs=" + mSrcs)
Steve McKay97b4be42016-01-20 15:09:35 -0800636 .append(", destination=" + stack)
637 .append("}")
638 .toString();
639 }
Steve McKaybbeba522016-01-13 17:17:39 -0800640}