blob: 8eabbe41f6663f0bf41af56c05db6dcf8f7c9aa8 [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;
Garfield, Tan48ef36f2016-06-09 12:04:22 -070024
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090025import static com.android.documentsui.OperationDialogFragment.DIALOG_TYPE_CONVERTED;
Steve McKayc83baa02016-01-06 18:32:13 -080026import static com.android.documentsui.Shared.DEBUG;
27import static com.android.documentsui.model.DocumentInfo.getCursorLong;
28import static com.android.documentsui.model.DocumentInfo.getCursorString;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090029import static com.android.documentsui.services.FileOperationService.EXTRA_DIALOG_TYPE;
Garfield, Tan48334772016-06-28 17:17:38 -070030import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION_TYPE;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090031import static com.android.documentsui.services.FileOperationService.EXTRA_SRC_LIST;
Garfield, Tan48334772016-06-28 17:17:38 -070032import static com.android.documentsui.services.FileOperationService.OPERATION_COPY;
Steve McKayc83baa02016-01-06 18:32:13 -080033
34import android.annotation.StringRes;
35import android.app.Notification;
36import android.app.Notification.Builder;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090037import android.app.PendingIntent;
Steve McKayc83baa02016-01-06 18:32:13 -080038import android.content.ContentProviderClient;
Garfield, Tanedce5542016-06-17 15:32:28 -070039import android.content.ContentResolver;
Steve McKayc83baa02016-01-06 18:32:13 -080040import android.content.Context;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090041import android.content.Intent;
Steve McKayc83baa02016-01-06 18:32:13 -080042import android.content.res.AssetFileDescriptor;
43import android.database.Cursor;
44import android.net.Uri;
45import android.os.CancellationSignal;
46import android.os.ParcelFileDescriptor;
47import android.os.RemoteException;
48import android.provider.DocumentsContract;
49import android.provider.DocumentsContract.Document;
50import android.text.format.DateUtils;
51import android.util.Log;
52import android.webkit.MimeTypeMap;
53
Garfield, Tanc20c6f62016-07-06 17:24:20 -070054import com.android.documentsui.DocumentsApplication;
Ben Kwafaa27202016-01-28 16:39:57 -080055import com.android.documentsui.Metrics;
Steve McKayc83baa02016-01-06 18:32:13 -080056import com.android.documentsui.R;
Garfield, Tanc20c6f62016-07-06 17:24:20 -070057import com.android.documentsui.RootsCache;
Garfield, Tan9666ce62016-07-12 11:02:09 -070058import com.android.documentsui.clipping.UrisSupplier;
Steve McKayc83baa02016-01-06 18:32:13 -080059import com.android.documentsui.model.DocumentInfo;
60import com.android.documentsui.model.DocumentStack;
Garfield, Tanedce5542016-06-17 15:32:28 -070061import com.android.documentsui.model.RootInfo;
Garfield, Tan48334772016-06-28 17:17:38 -070062import com.android.documentsui.services.FileOperationService.OpType;
Steve McKayc83baa02016-01-06 18:32:13 -080063
64import libcore.io.IoUtils;
65
66import java.io.FileNotFoundException;
67import java.io.IOException;
68import java.io.InputStream;
Steve McKayc83baa02016-01-06 18:32:13 -080069import java.text.NumberFormat;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090070import java.util.ArrayList;
Steve McKayc83baa02016-01-06 18:32:13 -080071import java.util.List;
72
73class CopyJob extends Job {
Steve McKay003097d2016-02-23 10:06:50 -080074
Steve McKayc83baa02016-01-06 18:32:13 -080075 private static final String TAG = "CopyJob";
Steve McKay003097d2016-02-23 10:06:50 -080076
Steve McKay97b4be42016-01-20 15:09:35 -080077 final List<DocumentInfo> mSrcs;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090078 final ArrayList<DocumentInfo> convertedFiles = new ArrayList<>();
Steve McKayc83baa02016-01-06 18:32:13 -080079
80 private long mStartTime = -1;
Steve McKay003097d2016-02-23 10:06:50 -080081
Steve McKayc83baa02016-01-06 18:32:13 -080082 private long mBatchSize;
Garfield, Tan48ef36f2016-06-09 12:04:22 -070083 private volatile long mBytesCopied;
Steve McKayc83baa02016-01-06 18:32:13 -080084 // Speed estimation
85 private long mBytesCopiedSample;
86 private long mSampleTime;
87 private long mSpeed;
88 private long mRemainingTime;
89
90 /**
Steve McKayc83baa02016-01-06 18:32:13 -080091 * @see @link {@link Job} constructor for most param descriptions.
Steve McKayc83baa02016-01-06 18:32:13 -080092 */
Garfield, Tan48334772016-06-28 17:17:38 -070093 CopyJob(Context service, Listener listener, String id, DocumentStack destination,
94 UrisSupplier srcs) {
95 this(service, listener, id, OPERATION_COPY, destination, srcs);
96 }
Steve McKaybbeba522016-01-13 17:17:39 -080097
Garfield, Tan48334772016-06-28 17:17:38 -070098 CopyJob(Context service, Listener listener, String id, @OpType int opType,
99 DocumentStack destination, UrisSupplier srcs) {
100 super(service, listener, id, opType, destination, srcs);
101
102 assert(srcs.getItemCount() > 0);
Steve McKaybbeba522016-01-13 17:17:39 -0800103
Garfield, Tanedce5542016-06-17 15:32:28 -0700104 // delay the initialization of it to setUp() because it may be IO extensive.
Garfield, Tan48334772016-06-28 17:17:38 -0700105 mSrcs = new ArrayList<>(srcs.getItemCount());
Steve McKayc83baa02016-01-06 18:32:13 -0800106 }
107
108 @Override
109 Builder createProgressBuilder() {
110 return super.createProgressBuilder(
Steve McKaybbeba522016-01-13 17:17:39 -0800111 service.getString(R.string.copy_notification_title),
Steve McKayc83baa02016-01-06 18:32:13 -0800112 R.drawable.ic_menu_copy,
Steve McKaybbeba522016-01-13 17:17:39 -0800113 service.getString(android.R.string.cancel),
Steve McKayc83baa02016-01-06 18:32:13 -0800114 R.drawable.ic_cab_cancel);
115 }
116
117 @Override
118 public Notification getSetupNotification() {
Steve McKaybbeba522016-01-13 17:17:39 -0800119 return getSetupNotification(service.getString(R.string.copy_preparing));
Steve McKayc83baa02016-01-06 18:32:13 -0800120 }
121
Steve McKayc83baa02016-01-06 18:32:13 -0800122 Notification getProgressNotification(@StringRes int msgId) {
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700123 updateRemainingTimeEstimate();
124
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900125 if (mBatchSize >= 0) {
126 double completed = (double) this.mBytesCopied / mBatchSize;
127 mProgressBuilder.setProgress(100, (int) (completed * 100), false);
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700128 mProgressBuilder.setSubText(
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900129 NumberFormat.getPercentInstance().format(completed));
130 } else {
131 // If the total file size failed to compute on some files, then show
132 // an indeterminate spinner. CopyJob would most likely fail on those
133 // files while copying, but would continue with another files.
134 // Also, if the total size is 0 bytes, show an indeterminate spinner.
135 mProgressBuilder.setProgress(0, 0, true);
136 }
137
Steve McKayc83baa02016-01-06 18:32:13 -0800138 if (mRemainingTime > 0) {
Steve McKaybbeba522016-01-13 17:17:39 -0800139 mProgressBuilder.setContentText(service.getString(msgId,
Steve McKayc83baa02016-01-06 18:32:13 -0800140 DateUtils.formatDuration(mRemainingTime)));
141 } else {
142 mProgressBuilder.setContentText(null);
143 }
144
Steve McKayc83baa02016-01-06 18:32:13 -0800145 return mProgressBuilder.build();
146 }
147
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700148 @Override
Steve McKayc83baa02016-01-06 18:32:13 -0800149 public Notification getProgressNotification() {
150 return getProgressNotification(R.string.copy_remaining);
151 }
152
153 void onBytesCopied(long numBytes) {
154 this.mBytesCopied += numBytes;
155 }
156
157 /**
158 * Generates an estimate of the remaining time in the copy.
159 */
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700160 private void updateRemainingTimeEstimate() {
Steve McKayc83baa02016-01-06 18:32:13 -0800161 long elapsedTime = elapsedRealtime() - mStartTime;
162
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700163 // mBytesCopied is modified in worker thread, but this method is called in monitor thread,
164 // so take a snapshot of mBytesCopied to make sure the updated estimate is consistent.
165 final long bytesCopied = mBytesCopied;
Garfield, Tanedce5542016-06-17 15:32:28 -0700166 final long sampleDuration = Math.max(elapsedTime - mSampleTime, 1L); // avoid dividing 0
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700167 final long sampleSpeed = ((bytesCopied - mBytesCopiedSample) * 1000) / sampleDuration;
Steve McKayc83baa02016-01-06 18:32:13 -0800168 if (mSpeed == 0) {
169 mSpeed = sampleSpeed;
170 } else {
171 mSpeed = ((3 * mSpeed) + sampleSpeed) / 4;
172 }
173
174 if (mSampleTime > 0 && mSpeed > 0) {
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700175 mRemainingTime = ((mBatchSize - bytesCopied) * 1000) / mSpeed;
Steve McKayc83baa02016-01-06 18:32:13 -0800176 } else {
177 mRemainingTime = 0;
178 }
179
180 mSampleTime = elapsedTime;
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700181 mBytesCopiedSample = bytesCopied;
Steve McKayc83baa02016-01-06 18:32:13 -0800182 }
183
184 @Override
185 Notification getFailureNotification() {
186 return getFailureNotification(
187 R.plurals.copy_error_notification_title, R.drawable.ic_menu_copy);
188 }
189
190 @Override
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900191 Notification getWarningNotification() {
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +0900192 final Intent navigateIntent = buildNavigateIntent(INTENT_TAG_WARNING);
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900193 navigateIntent.putExtra(EXTRA_DIALOG_TYPE, DIALOG_TYPE_CONVERTED);
Garfield, Tan48334772016-06-28 17:17:38 -0700194 navigateIntent.putExtra(EXTRA_OPERATION_TYPE, operationType);
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900195
196 navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, convertedFiles);
197
198 // TODO: Consider adding a dialog on tapping the notification with a list of
199 // converted files.
200 final Notification.Builder warningBuilder = new Notification.Builder(service)
201 .setContentTitle(service.getResources().getString(
202 R.string.notification_copy_files_converted_title))
203 .setContentText(service.getString(
204 R.string.notification_touch_for_details))
205 .setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent,
206 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
207 .setCategory(Notification.CATEGORY_ERROR)
208 .setSmallIcon(R.drawable.ic_menu_copy)
209 .setAutoCancel(true);
210 return warningBuilder.build();
211 }
212
213 @Override
Garfield, Tanedce5542016-06-17 15:32:28 -0700214 boolean setUp() {
Garfield, Tanedce5542016-06-17 15:32:28 -0700215 try {
216 buildDocumentList();
217 } catch (ResourceException e) {
218 Log.e(TAG, "Failed to get the list of docs.", e);
219 return false;
220 }
221
Garfield, Tanc20c6f62016-07-06 17:24:20 -0700222 // Check if user has canceled this task.
Garfield, Tanedce5542016-06-17 15:32:28 -0700223 if (isCanceled()) {
224 return false;
225 }
Steve McKayc83baa02016-01-06 18:32:13 -0800226
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900227 try {
228 mBatchSize = calculateSize(mSrcs);
229 } catch (ResourceException e) {
Steve McKayb1b08b22016-06-14 15:56:50 -0700230 Log.w(TAG, "Failed to calculate total size. Copying without progress.", e);
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900231 mBatchSize = -1;
232 }
Steve McKayc83baa02016-01-06 18:32:13 -0800233
Garfield, Tanc20c6f62016-07-06 17:24:20 -0700234 // Check if user has canceled this task. We should check it again here as user cancels
235 // tasks in main thread, but this is running in a worker thread. calculateSize() may
236 // take a long time during which user can cancel this task, and we don't want to waste
237 // resources doing useless large chunk of work.
238 if (isCanceled()) {
239 return false;
240 }
241
242 return checkSpace();
Garfield, Tanedce5542016-06-17 15:32:28 -0700243 }
244
245 @Override
246 void start() {
247 mStartTime = elapsedRealtime();
Steve McKayc83baa02016-01-06 18:32:13 -0800248 DocumentInfo srcInfo;
Ben Kwafaa27202016-01-28 16:39:57 -0800249 DocumentInfo dstInfo = stack.peek();
Steve McKay97b4be42016-01-20 15:09:35 -0800250 for (int i = 0; i < mSrcs.size() && !isCanceled(); ++i) {
251 srcInfo = mSrcs.get(i);
Steve McKayc83baa02016-01-06 18:32:13 -0800252
Steve McKayc83baa02016-01-06 18:32:13 -0800253 if (DEBUG) Log.d(TAG,
Steve McKaybbeba522016-01-13 17:17:39 -0800254 "Copying " + srcInfo.displayName + " (" + srcInfo.derivedUri + ")"
255 + " to " + dstInfo.displayName + " (" + dstInfo.derivedUri + ")");
Steve McKayc83baa02016-01-06 18:32:13 -0800256
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900257 try {
Steve McKayb1b08b22016-06-14 15:56:50 -0700258 if (dstInfo.equals(srcInfo) || isDescendentOf(srcInfo, dstInfo)) {
259 Log.e(TAG, "Skipping recursive copy of " + srcInfo.derivedUri);
260 onFileFailed(srcInfo);
261 } else {
262 processDocument(srcInfo, null, dstInfo);
263 }
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900264 } catch (ResourceException e) {
Steve McKayb1b08b22016-06-14 15:56:50 -0700265 Log.e(TAG, "Failed to copy " + srcInfo.derivedUri, e);
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900266 onFileFailed(srcInfo);
267 }
Steve McKayc83baa02016-01-06 18:32:13 -0800268 }
Ben Kwafaa27202016-01-28 16:39:57 -0800269 Metrics.logFileOperation(service, operationType, mSrcs, dstInfo);
Steve McKayc83baa02016-01-06 18:32:13 -0800270 }
271
Garfield, Tanedce5542016-06-17 15:32:28 -0700272 private void buildDocumentList() throws ResourceException {
273 try {
274 final ContentResolver resolver = appContext.getContentResolver();
Garfield, Tanb7e5f6b2016-06-30 18:27:47 -0700275 final Iterable<Uri> uris = srcs.getUris(appContext);
Garfield, Tan0ead7842016-07-21 11:10:44 -0700276
277 int docProcessed = 0;
Garfield, Tanedce5542016-06-17 15:32:28 -0700278 for (Uri uri : uris) {
279 DocumentInfo doc = DocumentInfo.fromUri(resolver, uri);
280 if (canCopy(doc, stack.root)) {
281 mSrcs.add(doc);
282 } else {
283 onFileFailed(doc);
284 }
Garfield, Tan0ead7842016-07-21 11:10:44 -0700285 ++docProcessed;
Garfield, Tanedce5542016-06-17 15:32:28 -0700286
287 if (isCanceled()) {
288 return;
289 }
290 }
Garfield, Tan0ead7842016-07-21 11:10:44 -0700291
292 // If docProcessed is different than the count claimed by UrisSupplier, add the number
293 // to failedFileCount.
294 failedFileCount += (srcs.getItemCount() - docProcessed);
Garfield, Tanedce5542016-06-17 15:32:28 -0700295 } catch(IOException e) {
Garfield, Tan48334772016-06-28 17:17:38 -0700296 failedFileCount += srcs.getItemCount();
Garfield, Tanedce5542016-06-17 15:32:28 -0700297 throw new ResourceException("Failed to open the list of docs to copy.", e);
298 }
299 }
300
301 private static boolean canCopy(DocumentInfo doc, RootInfo root) {
302 // Can't copy folders to downloads, because we don't show folders there.
303 return !root.isDownloads() || !doc.isDirectory();
304 }
305
Garfield, Tanc20c6f62016-07-06 17:24:20 -0700306 /**
307 * Checks whether the destination folder has enough space to take all source files.
308 * @return true if the root has enough space or doesn't provide free space info; otherwise false
309 */
310 boolean checkSpace() {
311 return checkSpace(mBatchSize);
312 }
313
314 /**
315 * Checks whether the destination folder has enough space to take files of batchSize
316 * @param batchSize the total size of files
317 * @return true if the root has enough space or doesn't provide free space info; otherwise false
318 */
319 final boolean checkSpace(long batchSize) {
320 // Default to be true because if batchSize or available space is invalid, we still let the
321 // copy start anyway.
322 boolean result = true;
323 if (batchSize >= 0) {
324 RootsCache cache = DocumentsApplication.getRootsCache(appContext);
325
326 // Query root info here instead of using stack.root because the number there may be
327 // stale.
328 RootInfo root = cache.getRootOneshot(stack.root.authority, stack.root.rootId, true);
329 if (root.availableBytes >= 0) {
330 result = (batchSize <= root.availableBytes);
331 } else {
332 Log.w(TAG, root.toString() + " doesn't provide available bytes.");
333 }
334 }
335
336 if (!result) {
337 failedFileCount += mSrcs.size();
338 failedFiles.addAll(mSrcs);
339 }
340
341 return result;
342 }
343
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900344 @Override
345 boolean hasWarnings() {
346 return !convertedFiles.isEmpty();
347 }
348
Steve McKayc83baa02016-01-06 18:32:13 -0800349 /**
350 * Logs progress on the current copy operation. Displays/Updates the progress notification.
351 *
352 * @param bytesCopied
353 */
354 private void makeCopyProgress(long bytesCopied) {
355 onBytesCopied(bytesCopied);
Steve McKayc83baa02016-01-06 18:32:13 -0800356 }
357
358 /**
359 * Copies a the given document to the given location.
360 *
Steve McKay97b4be42016-01-20 15:09:35 -0800361 * @param src DocumentInfos for the documents to copy.
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +0900362 * @param srcParent DocumentInfo for the parent of the document to process.
Steve McKayc83baa02016-01-06 18:32:13 -0800363 * @param dstDirInfo The destination directory.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900364 * @throws ResourceException
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +0900365 *
366 * TODO: Stop passing srcParent, as it's not used for copy, but for move only.
Steve McKayc83baa02016-01-06 18:32:13 -0800367 */
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900368 void processDocument(DocumentInfo src, DocumentInfo srcParent,
369 DocumentInfo dstDirInfo) throws ResourceException {
Steve McKayc83baa02016-01-06 18:32:13 -0800370
371 // TODO: When optimized copy kicks in, we'll not making any progress updates.
372 // For now. Local storage isn't using optimized copy.
373
Tomasz Mikolajewski4aa89672016-01-21 10:00:33 +0900374 // When copying within the same provider, try to use optimized copying.
Steve McKayc83baa02016-01-06 18:32:13 -0800375 // If not supported, then fallback to byte-by-byte copy/move.
Steve McKay97b4be42016-01-20 15:09:35 -0800376 if (src.authority.equals(dstDirInfo.authority)) {
377 if ((src.flags & Document.FLAG_SUPPORTS_COPY) != 0) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900378 try {
379 if (DocumentsContract.copyDocument(getClient(src), src.derivedUri,
Tomasz Mikolajewskife7f5362016-03-02 17:41:40 +0900380 dstDirInfo.derivedUri) != null) {
381 return;
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900382 }
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900383 } catch (RemoteException | RuntimeException e) {
Tomasz Mikolajewskife7f5362016-03-02 17:41:40 +0900384 Log.e(TAG, "Provider side copy failed for: " + src.derivedUri
Steve McKayb1b08b22016-06-14 15:56:50 -0700385 + " due to an exception.", e);
Steve McKayc83baa02016-01-06 18:32:13 -0800386 }
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700387
Tomasz Mikolajewskife7f5362016-03-02 17:41:40 +0900388 // If optimized copy fails, then fallback to byte-by-byte copy.
389 if (DEBUG) Log.d(TAG, "Fallback to byte-by-byte copy for: " + src.derivedUri);
Steve McKayc83baa02016-01-06 18:32:13 -0800390 }
391 }
392
393 // If we couldn't do an optimized copy...we fall back to vanilla byte copy.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900394 byteCopyDocument(src, dstDirInfo);
Steve McKayc83baa02016-01-06 18:32:13 -0800395 }
396
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900397 void byteCopyDocument(DocumentInfo src, DocumentInfo dest) throws ResourceException {
Steve McKayc83baa02016-01-06 18:32:13 -0800398 final String dstMimeType;
399 final String dstDisplayName;
400
Steve McKay97b4be42016-01-20 15:09:35 -0800401 if (DEBUG) Log.d(TAG, "Doing byte copy of document: " + src);
Steve McKayc83baa02016-01-06 18:32:13 -0800402 // If the file is virtual, but can be converted to another format, then try to copy it
403 // as such format. Also, append an extension for the target mime type (if known).
Steve McKay97b4be42016-01-20 15:09:35 -0800404 if (src.isVirtualDocument()) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900405 String[] streamTypes = null;
406 try {
407 streamTypes = getContentResolver().getStreamTypes(src.derivedUri, "*/*");
408 } catch (RuntimeException e) {
409 throw new ResourceException(
410 "Failed to obtain streamable types for %s due to an exception.",
411 src.derivedUri, e);
412 }
Steve McKayc83baa02016-01-06 18:32:13 -0800413 if (streamTypes != null && streamTypes.length > 0) {
414 dstMimeType = streamTypes[0];
415 final String extension = MimeTypeMap.getSingleton().
416 getExtensionFromMimeType(dstMimeType);
Steve McKay97b4be42016-01-20 15:09:35 -0800417 dstDisplayName = src.displayName +
418 (extension != null ? "." + extension : src.displayName);
Steve McKayc83baa02016-01-06 18:32:13 -0800419 } else {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900420 throw new ResourceException("Cannot copy virtual file %s. No streamable formats "
421 + "available.", src.derivedUri);
Steve McKayc83baa02016-01-06 18:32:13 -0800422 }
423 } else {
Steve McKay97b4be42016-01-20 15:09:35 -0800424 dstMimeType = src.mimeType;
425 dstDisplayName = src.displayName;
Steve McKayc83baa02016-01-06 18:32:13 -0800426 }
427
428 // Create the target document (either a file or a directory), then copy recursively the
429 // contents (bytes or children).
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900430 Uri dstUri = null;
431 try {
432 dstUri = DocumentsContract.createDocument(
433 getClient(dest), dest.derivedUri, dstMimeType, dstDisplayName);
434 } catch (RemoteException | RuntimeException e) {
435 throw new ResourceException(
436 "Couldn't create destination document " + dstDisplayName + " in directory %s "
437 + "due to an exception.", dest.derivedUri, e);
438 }
Steve McKayc83baa02016-01-06 18:32:13 -0800439 if (dstUri == null) {
440 // If this is a directory, the entire subdir will not be copied over.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900441 throw new ResourceException(
442 "Couldn't create destination document " + dstDisplayName + " in directory %s.",
443 dest.derivedUri);
Steve McKayc83baa02016-01-06 18:32:13 -0800444 }
445
446 DocumentInfo dstInfo = null;
447 try {
448 dstInfo = DocumentInfo.fromUri(getContentResolver(), dstUri);
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900449 } catch (FileNotFoundException | RuntimeException e) {
450 throw new ResourceException("Could not load DocumentInfo for newly created file %s.",
451 dstUri);
Steve McKayc83baa02016-01-06 18:32:13 -0800452 }
453
Steve McKay97b4be42016-01-20 15:09:35 -0800454 if (Document.MIME_TYPE_DIR.equals(src.mimeType)) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900455 copyDirectoryHelper(src, dstInfo);
Steve McKayc83baa02016-01-06 18:32:13 -0800456 } else {
Tomasz Mikolajewski8fe54982016-02-23 15:12:54 +0900457 copyFileHelper(src, dstInfo, dest, dstMimeType);
Steve McKayc83baa02016-01-06 18:32:13 -0800458 }
Steve McKayc83baa02016-01-06 18:32:13 -0800459 }
460
461 /**
462 * Handles recursion into a directory and copying its contents. Note that in linux terms, this
463 * does the equivalent of "cp src/* dst", not "cp -r src dst".
464 *
Steve McKay97b4be42016-01-20 15:09:35 -0800465 * @param srcDir Info of the directory to copy from. The routine will copy the directory's
Steve McKayc83baa02016-01-06 18:32:13 -0800466 * contents, not the directory itself.
Steve McKay97b4be42016-01-20 15:09:35 -0800467 * @param destDir Info of the directory to copy to. Must be created beforehand.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900468 * @throws ResourceException
Steve McKayc83baa02016-01-06 18:32:13 -0800469 */
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900470 private void copyDirectoryHelper(DocumentInfo srcDir, DocumentInfo destDir)
471 throws ResourceException {
Steve McKayc83baa02016-01-06 18:32:13 -0800472 // Recurse into directories. Copy children into the new subdirectory.
473 final String queryColumns[] = new String[] {
474 Document.COLUMN_DISPLAY_NAME,
475 Document.COLUMN_DOCUMENT_ID,
476 Document.COLUMN_MIME_TYPE,
477 Document.COLUMN_SIZE,
478 Document.COLUMN_FLAGS
479 };
480 Cursor cursor = null;
481 boolean success = true;
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900482 // Iterate over srcs in the directory; copy to the destination directory.
483 final Uri queryUri = buildChildDocumentsUri(srcDir.authority, srcDir.documentId);
Steve McKayc83baa02016-01-06 18:32:13 -0800484 try {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900485 try {
486 cursor = getClient(srcDir).query(queryUri, queryColumns, null, null, null);
487 } catch (RemoteException | RuntimeException e) {
488 throw new ResourceException("Failed to query children of %s due to an exception.",
489 srcDir.derivedUri, e);
Steve McKayc83baa02016-01-06 18:32:13 -0800490 }
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900491
492 DocumentInfo src;
493 while (cursor.moveToNext() && !isCanceled()) {
494 try {
495 src = DocumentInfo.fromCursor(cursor, srcDir.authority);
496 processDocument(src, srcDir, destDir);
497 } catch (RuntimeException e) {
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700498 Log.e(TAG, String.format(
499 "Failed to recursively process a file %s due to an exception.",
500 srcDir.derivedUri.toString()), e);
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900501 success = false;
502 }
503 }
504 } catch (RuntimeException e) {
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700505 Log.e(TAG, String.format(
506 "Failed to copy a file %s to %s. ",
507 srcDir.derivedUri.toString(), destDir.derivedUri.toString()), e);
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900508 success = false;
Steve McKayc83baa02016-01-06 18:32:13 -0800509 } finally {
510 IoUtils.closeQuietly(cursor);
511 }
512
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900513 if (!success) {
514 throw new RuntimeException("Some files failed to copy during a recursive "
515 + "directory copy.");
516 }
Steve McKayc83baa02016-01-06 18:32:13 -0800517 }
518
519 /**
520 * Handles copying a single file.
521 *
Tomasz Mikolajewski8fe54982016-02-23 15:12:54 +0900522 * @param src Info of the file to copy from.
523 * @param dest Info of the *file* to copy to. Must be created beforehand.
524 * @param destParent Info of the parent of the destination.
Steve McKayc83baa02016-01-06 18:32:13 -0800525 * @param mimeType Mime type for the target. Can be different than source for virtual files.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900526 * @throws ResourceException
Steve McKayc83baa02016-01-06 18:32:13 -0800527 */
Tomasz Mikolajewski8fe54982016-02-23 15:12:54 +0900528 private void copyFileHelper(DocumentInfo src, DocumentInfo dest, DocumentInfo destParent,
529 String mimeType) throws ResourceException {
Steve McKayc83baa02016-01-06 18:32:13 -0800530 CancellationSignal canceller = new CancellationSignal();
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900531 AssetFileDescriptor srcFileAsAsset = null;
Steve McKayc83baa02016-01-06 18:32:13 -0800532 ParcelFileDescriptor srcFile = null;
533 ParcelFileDescriptor dstFile = null;
Steve McKay97b4be42016-01-20 15:09:35 -0800534 InputStream in = null;
Daichi Hirono75512402016-03-28 16:07:45 +0900535 ParcelFileDescriptor.AutoCloseOutputStream out = null;
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900536 boolean success = false;
Steve McKayc83baa02016-01-06 18:32:13 -0800537
Steve McKayc83baa02016-01-06 18:32:13 -0800538 try {
539 // If the file is virtual, but can be converted to another format, then try to copy it
540 // as such format.
Steve McKay97b4be42016-01-20 15:09:35 -0800541 if (src.isVirtualDocument()) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900542 try {
543 srcFileAsAsset = getClient(src).openTypedAssetFileDescriptor(
Steve McKay97b4be42016-01-20 15:09:35 -0800544 src.derivedUri, mimeType, null, canceller);
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900545 } catch (FileNotFoundException | RemoteException | RuntimeException e) {
546 throw new ResourceException("Failed to open a file as asset for %s due to an "
547 + "exception.", src.derivedUri, e);
548 }
Steve McKayc83baa02016-01-06 18:32:13 -0800549 srcFile = srcFileAsAsset.getParcelFileDescriptor();
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900550 try {
551 in = new AssetFileDescriptor.AutoCloseInputStream(srcFileAsAsset);
552 } catch (IOException e) {
553 throw new ResourceException("Failed to open a file input stream for %s due "
554 + "an exception.", src.derivedUri, e);
555 }
Steve McKayc83baa02016-01-06 18:32:13 -0800556 } else {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900557 try {
558 srcFile = getClient(src).openFile(src.derivedUri, "r", canceller);
559 } catch (FileNotFoundException | RemoteException | RuntimeException e) {
560 throw new ResourceException(
561 "Failed to open a file for %s due to an exception.", src.derivedUri, e);
562 }
Steve McKay97b4be42016-01-20 15:09:35 -0800563 in = new ParcelFileDescriptor.AutoCloseInputStream(srcFile);
Steve McKayc83baa02016-01-06 18:32:13 -0800564 }
565
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900566 try {
567 dstFile = getClient(dest).openFile(dest.derivedUri, "w", canceller);
568 } catch (FileNotFoundException | RemoteException | RuntimeException e) {
569 throw new ResourceException("Failed to open the destination file %s for writing "
570 + "due to an exception.", dest.derivedUri, e);
571 }
Steve McKay97b4be42016-01-20 15:09:35 -0800572 out = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);
Steve McKayc83baa02016-01-06 18:32:13 -0800573
Steve McKaybbeba522016-01-13 17:17:39 -0800574 byte[] buffer = new byte[32 * 1024];
Steve McKayc83baa02016-01-06 18:32:13 -0800575 int len;
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900576 try {
577 while ((len = in.read(buffer)) != -1) {
578 if (isCanceled()) {
Steve McKay003097d2016-02-23 10:06:50 -0800579 if (DEBUG) Log.d(TAG, "Canceled copy mid-copy of: " + src.derivedUri);
580 return;
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900581 }
582 out.write(buffer, 0, len);
583 makeCopyProgress(len);
Steve McKayc83baa02016-01-06 18:32:13 -0800584 }
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900585
Daichi Hirono75512402016-03-28 16:07:45 +0900586 // Need to invoke IoUtils.close explicitly to avoid from ignoring errors at flush.
587 IoUtils.close(dstFile.getFileDescriptor());
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900588 srcFile.checkError();
589 } catch (IOException e) {
590 throw new ResourceException(
591 "Failed to copy bytes from %s to %s due to an IO exception.",
592 src.derivedUri, dest.derivedUri, e);
Steve McKayc83baa02016-01-06 18:32:13 -0800593 }
594
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900595 if (src.isVirtualDocument()) {
596 convertedFiles.add(src);
Steve McKayc83baa02016-01-06 18:32:13 -0800597 }
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900598
599 success = true;
Steve McKayc83baa02016-01-06 18:32:13 -0800600 } finally {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900601 if (!success) {
602 if (dstFile != null) {
603 try {
604 dstFile.closeWithError("Error copying bytes.");
605 } catch (IOException closeError) {
606 Log.w(TAG, "Error closing destination.", closeError);
607 }
608 }
609
610 if (DEBUG) Log.d(TAG, "Cleaning up failed operation leftovers.");
611 canceller.cancel();
612 try {
Tomasz Mikolajewski8fe54982016-02-23 15:12:54 +0900613 deleteDocument(dest, destParent);
614 } catch (ResourceException e) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900615 Log.w(TAG, "Failed to cleanup after copy error: " + src.derivedUri, e);
616 }
617 }
618
Steve McKayc83baa02016-01-06 18:32:13 -0800619 // This also ensures the file descriptors are closed.
Steve McKay97b4be42016-01-20 15:09:35 -0800620 IoUtils.closeQuietly(in);
621 IoUtils.closeQuietly(out);
Steve McKayc83baa02016-01-06 18:32:13 -0800622 }
Steve McKayc83baa02016-01-06 18:32:13 -0800623 }
624
625 /**
626 * Calculates the cumulative size of all the documents in the list. Directories are recursed
627 * into and totaled up.
628 *
629 * @param srcs
630 * @return Size in bytes.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900631 * @throws ResourceException
Steve McKayc83baa02016-01-06 18:32:13 -0800632 */
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900633 private long calculateSize(List<DocumentInfo> srcs) throws ResourceException {
Steve McKayc83baa02016-01-06 18:32:13 -0800634 long result = 0;
635
636 for (DocumentInfo src : srcs) {
637 if (src.isDirectory()) {
638 // Directories need to be recursed into.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900639 try {
640 result += calculateFileSizesRecursively(getClient(src), src.derivedUri);
641 } catch (RemoteException e) {
642 throw new ResourceException("Failed to obtain the client for %s.",
Garfield, Tanc20c6f62016-07-06 17:24:20 -0700643 src.derivedUri, e);
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900644 }
Steve McKayc83baa02016-01-06 18:32:13 -0800645 } else {
646 result += src.size;
647 }
Garfield, Tanedce5542016-06-17 15:32:28 -0700648
649 if (isCanceled()) {
650 return result;
651 }
Steve McKayc83baa02016-01-06 18:32:13 -0800652 }
653 return result;
654 }
655
656 /**
657 * Calculates (recursively) the cumulative size of all the files under the given directory.
658 *
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900659 * @throws ResourceException
Steve McKayc83baa02016-01-06 18:32:13 -0800660 */
Garfield, Tanc20c6f62016-07-06 17:24:20 -0700661 long calculateFileSizesRecursively(
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900662 ContentProviderClient client, Uri uri) throws ResourceException {
Steve McKayc83baa02016-01-06 18:32:13 -0800663 final String authority = uri.getAuthority();
Steve McKaybbeba522016-01-13 17:17:39 -0800664 final Uri queryUri = buildChildDocumentsUri(authority, getDocumentId(uri));
Steve McKayc83baa02016-01-06 18:32:13 -0800665 final String queryColumns[] = new String[] {
666 Document.COLUMN_DOCUMENT_ID,
667 Document.COLUMN_MIME_TYPE,
668 Document.COLUMN_SIZE
669 };
670
671 long result = 0;
672 Cursor cursor = null;
673 try {
674 cursor = client.query(queryUri, queryColumns, null, null, null);
Garfield, Tanedce5542016-06-17 15:32:28 -0700675 while (cursor.moveToNext() && !isCanceled()) {
Steve McKayc83baa02016-01-06 18:32:13 -0800676 if (Document.MIME_TYPE_DIR.equals(
677 getCursorString(cursor, Document.COLUMN_MIME_TYPE))) {
678 // Recurse into directories.
Steve McKaybbeba522016-01-13 17:17:39 -0800679 final Uri dirUri = buildDocumentUri(authority,
Steve McKayc83baa02016-01-06 18:32:13 -0800680 getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
681 result += calculateFileSizesRecursively(client, dirUri);
682 } else {
683 // This may return -1 if the size isn't defined. Ignore those cases.
684 long size = getCursorLong(cursor, Document.COLUMN_SIZE);
685 result += size > 0 ? size : 0;
686 }
687 }
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900688 } catch (RemoteException | RuntimeException e) {
689 throw new ResourceException(
690 "Failed to calculate size for %s due to an exception.", uri, e);
Steve McKayc83baa02016-01-06 18:32:13 -0800691 } finally {
692 IoUtils.closeQuietly(cursor);
693 }
694
695 return result;
696 }
697
Steve McKayc83baa02016-01-06 18:32:13 -0800698 /**
699 * Returns true if {@code doc} is a descendant of {@code parentDoc}.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900700 * @throws ResourceException
Steve McKayc83baa02016-01-06 18:32:13 -0800701 */
Steve McKay97b4be42016-01-20 15:09:35 -0800702 boolean isDescendentOf(DocumentInfo doc, DocumentInfo parent)
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900703 throws ResourceException {
Steve McKay97b4be42016-01-20 15:09:35 -0800704 if (parent.isDirectory() && doc.authority.equals(parent.authority)) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900705 try {
706 return isChildDocument(getClient(doc), doc.derivedUri, parent.derivedUri);
707 } catch (RemoteException | RuntimeException e) {
708 throw new ResourceException(
709 "Failed to check if %s is a child of %s due to an exception.",
710 doc.derivedUri, parent.derivedUri, e);
711 }
Steve McKayc83baa02016-01-06 18:32:13 -0800712 }
713 return false;
714 }
Steve McKaybbeba522016-01-13 17:17:39 -0800715
Steve McKay97b4be42016-01-20 15:09:35 -0800716 @Override
717 public String toString() {
718 return new StringBuilder()
719 .append("CopyJob")
720 .append("{")
721 .append("id=" + id)
Garfield, Tan48334772016-06-28 17:17:38 -0700722 .append(", docs=" + srcs)
Steve McKay97b4be42016-01-20 15:09:35 -0800723 .append(", destination=" + stack)
724 .append("}")
725 .toString();
726 }
Steve McKaybbeba522016-01-13 17:17:39 -0800727}