blob: 1a3d94c56e8485506b95bfad7c701ee897ff879c [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 McKayd0805062016-09-15 14:30:38 -070025import static com.android.documentsui.base.DocumentInfo.getCursorLong;
26import static com.android.documentsui.base.DocumentInfo.getCursorString;
Steve McKayd9caa6a2016-09-15 16:36:45 -070027import static com.android.documentsui.base.Shared.DEBUG;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090028import static com.android.documentsui.services.FileOperationService.EXTRA_DIALOG_TYPE;
Garfield, Tan48334772016-06-28 17:17:38 -070029import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION_TYPE;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090030import static com.android.documentsui.services.FileOperationService.EXTRA_SRC_LIST;
Garfield, Tan48334772016-06-28 17:17:38 -070031import 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;
Garfield, Tanedce5542016-06-17 15:32:28 -070038import android.content.ContentResolver;
Steve McKayc83baa02016-01-06 18:32:13 -080039import android.content.Context;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090040import android.content.Intent;
Steve McKayc83baa02016-01-06 18:32:13 -080041import android.content.res.AssetFileDescriptor;
42import android.database.Cursor;
43import android.net.Uri;
44import android.os.CancellationSignal;
45import android.os.ParcelFileDescriptor;
46import android.os.RemoteException;
47import android.provider.DocumentsContract;
48import android.provider.DocumentsContract.Document;
Daichi Hirono0f3b4bd2016-11-16 15:42:42 +090049import android.system.ErrnoException;
50import android.system.Os;
51import android.system.OsConstants;
Steve McKayc83baa02016-01-06 18:32:13 -080052import android.text.format.DateUtils;
53import android.util.Log;
54import android.webkit.MimeTypeMap;
55
Garfield, Tanc20c6f62016-07-06 17:24:20 -070056import com.android.documentsui.DocumentsApplication;
Ben Kwafaa27202016-01-28 16:39:57 -080057import com.android.documentsui.Metrics;
Steve McKayc83baa02016-01-06 18:32:13 -080058import com.android.documentsui.R;
Steve McKayd0805062016-09-15 14:30:38 -070059import com.android.documentsui.base.DocumentInfo;
60import com.android.documentsui.base.DocumentStack;
61import com.android.documentsui.base.RootInfo;
Garfield, Tan9666ce62016-07-12 11:02:09 -070062import com.android.documentsui.clipping.UrisSupplier;
Steve McKayd9caa6a2016-09-15 16:36:45 -070063import com.android.documentsui.roots.RootsCache;
Garfield, Tan48334772016-06-28 17:17:38 -070064import com.android.documentsui.services.FileOperationService.OpType;
Steve McKayc83baa02016-01-06 18:32:13 -080065
66import libcore.io.IoUtils;
67
68import java.io.FileNotFoundException;
69import java.io.IOException;
70import java.io.InputStream;
Daichi Hirono0f3b4bd2016-11-16 15:42:42 +090071import java.io.SyncFailedException;
Steve McKayc83baa02016-01-06 18:32:13 -080072import java.text.NumberFormat;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090073import java.util.ArrayList;
Steve McKayc83baa02016-01-06 18:32:13 -080074import java.util.List;
75
76class CopyJob extends Job {
Steve McKay003097d2016-02-23 10:06:50 -080077
Steve McKayc83baa02016-01-06 18:32:13 -080078 private static final String TAG = "CopyJob";
Steve McKay003097d2016-02-23 10:06:50 -080079
Steve McKay97b4be42016-01-20 15:09:35 -080080 final List<DocumentInfo> mSrcs;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090081 final ArrayList<DocumentInfo> convertedFiles = new ArrayList<>();
Steve McKayc83baa02016-01-06 18:32:13 -080082
83 private long mStartTime = -1;
Steve McKay003097d2016-02-23 10:06:50 -080084
Steve McKayc83baa02016-01-06 18:32:13 -080085 private long mBatchSize;
Garfield, Tan48ef36f2016-06-09 12:04:22 -070086 private volatile long mBytesCopied;
Steve McKayc83baa02016-01-06 18:32:13 -080087 // Speed estimation
88 private long mBytesCopiedSample;
89 private long mSampleTime;
90 private long mSpeed;
91 private long mRemainingTime;
92
93 /**
Steve McKayc83baa02016-01-06 18:32:13 -080094 * @see @link {@link Job} constructor for most param descriptions.
Steve McKayc83baa02016-01-06 18:32:13 -080095 */
Garfield, Tan48334772016-06-28 17:17:38 -070096 CopyJob(Context service, Listener listener, String id, DocumentStack destination,
97 UrisSupplier srcs) {
98 this(service, listener, id, OPERATION_COPY, destination, srcs);
99 }
Steve McKaybbeba522016-01-13 17:17:39 -0800100
Garfield, Tan48334772016-06-28 17:17:38 -0700101 CopyJob(Context service, Listener listener, String id, @OpType int opType,
102 DocumentStack destination, UrisSupplier srcs) {
103 super(service, listener, id, opType, destination, srcs);
104
105 assert(srcs.getItemCount() > 0);
Steve McKaybbeba522016-01-13 17:17:39 -0800106
Garfield, Tanedce5542016-06-17 15:32:28 -0700107 // delay the initialization of it to setUp() because it may be IO extensive.
Garfield, Tan48334772016-06-28 17:17:38 -0700108 mSrcs = new ArrayList<>(srcs.getItemCount());
Steve McKayc83baa02016-01-06 18:32:13 -0800109 }
110
111 @Override
112 Builder createProgressBuilder() {
113 return super.createProgressBuilder(
Steve McKaybbeba522016-01-13 17:17:39 -0800114 service.getString(R.string.copy_notification_title),
Steve McKayc83baa02016-01-06 18:32:13 -0800115 R.drawable.ic_menu_copy,
Steve McKaybbeba522016-01-13 17:17:39 -0800116 service.getString(android.R.string.cancel),
Steve McKayc83baa02016-01-06 18:32:13 -0800117 R.drawable.ic_cab_cancel);
118 }
119
120 @Override
121 public Notification getSetupNotification() {
Steve McKaybbeba522016-01-13 17:17:39 -0800122 return getSetupNotification(service.getString(R.string.copy_preparing));
Steve McKayc83baa02016-01-06 18:32:13 -0800123 }
124
Steve McKayc83baa02016-01-06 18:32:13 -0800125 Notification getProgressNotification(@StringRes int msgId) {
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700126 updateRemainingTimeEstimate();
127
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900128 if (mBatchSize >= 0) {
129 double completed = (double) this.mBytesCopied / mBatchSize;
130 mProgressBuilder.setProgress(100, (int) (completed * 100), false);
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700131 mProgressBuilder.setSubText(
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900132 NumberFormat.getPercentInstance().format(completed));
133 } else {
134 // If the total file size failed to compute on some files, then show
135 // an indeterminate spinner. CopyJob would most likely fail on those
136 // files while copying, but would continue with another files.
137 // Also, if the total size is 0 bytes, show an indeterminate spinner.
138 mProgressBuilder.setProgress(0, 0, true);
139 }
140
Steve McKayc83baa02016-01-06 18:32:13 -0800141 if (mRemainingTime > 0) {
Steve McKaybbeba522016-01-13 17:17:39 -0800142 mProgressBuilder.setContentText(service.getString(msgId,
Steve McKayc83baa02016-01-06 18:32:13 -0800143 DateUtils.formatDuration(mRemainingTime)));
144 } else {
145 mProgressBuilder.setContentText(null);
146 }
147
Steve McKayc83baa02016-01-06 18:32:13 -0800148 return mProgressBuilder.build();
149 }
150
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700151 @Override
Steve McKayc83baa02016-01-06 18:32:13 -0800152 public Notification getProgressNotification() {
153 return getProgressNotification(R.string.copy_remaining);
154 }
155
156 void onBytesCopied(long numBytes) {
157 this.mBytesCopied += numBytes;
158 }
159
160 /**
161 * Generates an estimate of the remaining time in the copy.
162 */
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700163 private void updateRemainingTimeEstimate() {
Steve McKayc83baa02016-01-06 18:32:13 -0800164 long elapsedTime = elapsedRealtime() - mStartTime;
165
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700166 // mBytesCopied is modified in worker thread, but this method is called in monitor thread,
167 // so take a snapshot of mBytesCopied to make sure the updated estimate is consistent.
168 final long bytesCopied = mBytesCopied;
Garfield, Tanedce5542016-06-17 15:32:28 -0700169 final long sampleDuration = Math.max(elapsedTime - mSampleTime, 1L); // avoid dividing 0
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700170 final long sampleSpeed = ((bytesCopied - mBytesCopiedSample) * 1000) / sampleDuration;
Steve McKayc83baa02016-01-06 18:32:13 -0800171 if (mSpeed == 0) {
172 mSpeed = sampleSpeed;
173 } else {
174 mSpeed = ((3 * mSpeed) + sampleSpeed) / 4;
175 }
176
177 if (mSampleTime > 0 && mSpeed > 0) {
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700178 mRemainingTime = ((mBatchSize - bytesCopied) * 1000) / mSpeed;
Steve McKayc83baa02016-01-06 18:32:13 -0800179 } else {
180 mRemainingTime = 0;
181 }
182
183 mSampleTime = elapsedTime;
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700184 mBytesCopiedSample = bytesCopied;
Steve McKayc83baa02016-01-06 18:32:13 -0800185 }
186
187 @Override
188 Notification getFailureNotification() {
189 return getFailureNotification(
190 R.plurals.copy_error_notification_title, R.drawable.ic_menu_copy);
191 }
192
193 @Override
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900194 Notification getWarningNotification() {
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +0900195 final Intent navigateIntent = buildNavigateIntent(INTENT_TAG_WARNING);
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900196 navigateIntent.putExtra(EXTRA_DIALOG_TYPE, DIALOG_TYPE_CONVERTED);
Garfield, Tan48334772016-06-28 17:17:38 -0700197 navigateIntent.putExtra(EXTRA_OPERATION_TYPE, operationType);
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900198
199 navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, convertedFiles);
200
201 // TODO: Consider adding a dialog on tapping the notification with a list of
202 // converted files.
203 final Notification.Builder warningBuilder = new Notification.Builder(service)
204 .setContentTitle(service.getResources().getString(
205 R.string.notification_copy_files_converted_title))
206 .setContentText(service.getString(
207 R.string.notification_touch_for_details))
208 .setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent,
209 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
210 .setCategory(Notification.CATEGORY_ERROR)
211 .setSmallIcon(R.drawable.ic_menu_copy)
212 .setAutoCancel(true);
213 return warningBuilder.build();
214 }
215
216 @Override
Garfield, Tanedce5542016-06-17 15:32:28 -0700217 boolean setUp() {
Garfield, Tanedce5542016-06-17 15:32:28 -0700218 try {
219 buildDocumentList();
220 } catch (ResourceException e) {
221 Log.e(TAG, "Failed to get the list of docs.", e);
222 return false;
223 }
224
Garfield, Tanc20c6f62016-07-06 17:24:20 -0700225 // Check if user has canceled this task.
Garfield, Tanedce5542016-06-17 15:32:28 -0700226 if (isCanceled()) {
227 return false;
228 }
Steve McKayc83baa02016-01-06 18:32:13 -0800229
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900230 try {
231 mBatchSize = calculateSize(mSrcs);
232 } catch (ResourceException e) {
Steve McKayb1b08b22016-06-14 15:56:50 -0700233 Log.w(TAG, "Failed to calculate total size. Copying without progress.", e);
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900234 mBatchSize = -1;
235 }
Steve McKayc83baa02016-01-06 18:32:13 -0800236
Garfield, Tanc20c6f62016-07-06 17:24:20 -0700237 // Check if user has canceled this task. We should check it again here as user cancels
238 // tasks in main thread, but this is running in a worker thread. calculateSize() may
239 // take a long time during which user can cancel this task, and we don't want to waste
240 // resources doing useless large chunk of work.
241 if (isCanceled()) {
242 return false;
243 }
244
245 return checkSpace();
Garfield, Tanedce5542016-06-17 15:32:28 -0700246 }
247
248 @Override
249 void start() {
250 mStartTime = elapsedRealtime();
Steve McKayc83baa02016-01-06 18:32:13 -0800251 DocumentInfo srcInfo;
Ben Kwafaa27202016-01-28 16:39:57 -0800252 DocumentInfo dstInfo = stack.peek();
Steve McKay97b4be42016-01-20 15:09:35 -0800253 for (int i = 0; i < mSrcs.size() && !isCanceled(); ++i) {
254 srcInfo = mSrcs.get(i);
Steve McKayc83baa02016-01-06 18:32:13 -0800255
Steve McKayc83baa02016-01-06 18:32:13 -0800256 if (DEBUG) Log.d(TAG,
Steve McKaybbeba522016-01-13 17:17:39 -0800257 "Copying " + srcInfo.displayName + " (" + srcInfo.derivedUri + ")"
258 + " to " + dstInfo.displayName + " (" + dstInfo.derivedUri + ")");
Steve McKayc83baa02016-01-06 18:32:13 -0800259
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900260 try {
Steve McKayb1b08b22016-06-14 15:56:50 -0700261 if (dstInfo.equals(srcInfo) || isDescendentOf(srcInfo, dstInfo)) {
262 Log.e(TAG, "Skipping recursive copy of " + srcInfo.derivedUri);
263 onFileFailed(srcInfo);
264 } else {
265 processDocument(srcInfo, null, dstInfo);
266 }
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900267 } catch (ResourceException e) {
Steve McKayb1b08b22016-06-14 15:56:50 -0700268 Log.e(TAG, "Failed to copy " + srcInfo.derivedUri, e);
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900269 onFileFailed(srcInfo);
270 }
Steve McKayc83baa02016-01-06 18:32:13 -0800271 }
Ben Kwafaa27202016-01-28 16:39:57 -0800272 Metrics.logFileOperation(service, operationType, mSrcs, dstInfo);
Steve McKayc83baa02016-01-06 18:32:13 -0800273 }
274
Garfield, Tanedce5542016-06-17 15:32:28 -0700275 private void buildDocumentList() throws ResourceException {
276 try {
277 final ContentResolver resolver = appContext.getContentResolver();
Garfield, Tanb7e5f6b2016-06-30 18:27:47 -0700278 final Iterable<Uri> uris = srcs.getUris(appContext);
Garfield, Tan0ead7842016-07-21 11:10:44 -0700279
280 int docProcessed = 0;
Garfield, Tanedce5542016-06-17 15:32:28 -0700281 for (Uri uri : uris) {
282 DocumentInfo doc = DocumentInfo.fromUri(resolver, uri);
Garfield Tan2a837422016-10-19 11:50:45 -0700283 if (canCopy(doc, stack.getRoot())) {
Garfield, Tanedce5542016-06-17 15:32:28 -0700284 mSrcs.add(doc);
285 } else {
286 onFileFailed(doc);
287 }
Garfield, Tan0ead7842016-07-21 11:10:44 -0700288 ++docProcessed;
Garfield, Tanedce5542016-06-17 15:32:28 -0700289
290 if (isCanceled()) {
291 return;
292 }
293 }
Garfield, Tan0ead7842016-07-21 11:10:44 -0700294
295 // If docProcessed is different than the count claimed by UrisSupplier, add the number
296 // to failedFileCount.
297 failedFileCount += (srcs.getItemCount() - docProcessed);
Garfield, Tanedce5542016-06-17 15:32:28 -0700298 } catch(IOException e) {
Garfield, Tan48334772016-06-28 17:17:38 -0700299 failedFileCount += srcs.getItemCount();
Garfield, Tanedce5542016-06-17 15:32:28 -0700300 throw new ResourceException("Failed to open the list of docs to copy.", e);
301 }
302 }
303
304 private static boolean canCopy(DocumentInfo doc, RootInfo root) {
305 // Can't copy folders to downloads, because we don't show folders there.
306 return !root.isDownloads() || !doc.isDirectory();
307 }
308
Garfield, Tanc20c6f62016-07-06 17:24:20 -0700309 /**
310 * Checks whether the destination folder has enough space to take all source files.
311 * @return true if the root has enough space or doesn't provide free space info; otherwise false
312 */
313 boolean checkSpace() {
314 return checkSpace(mBatchSize);
315 }
316
317 /**
318 * Checks whether the destination folder has enough space to take files of batchSize
319 * @param batchSize the total size of files
320 * @return true if the root has enough space or doesn't provide free space info; otherwise false
321 */
322 final boolean checkSpace(long batchSize) {
323 // Default to be true because if batchSize or available space is invalid, we still let the
324 // copy start anyway.
325 boolean result = true;
326 if (batchSize >= 0) {
327 RootsCache cache = DocumentsApplication.getRootsCache(appContext);
328
Garfield Tan2a837422016-10-19 11:50:45 -0700329 RootInfo root = stack.getRoot();
Garfield, Tanc20c6f62016-07-06 17:24:20 -0700330 // Query root info here instead of using stack.root because the number there may be
331 // stale.
Garfield Tan2a837422016-10-19 11:50:45 -0700332 root = cache.getRootOneshot(root.authority, root.rootId, true);
Garfield, Tanc20c6f62016-07-06 17:24:20 -0700333 if (root.availableBytes >= 0) {
334 result = (batchSize <= root.availableBytes);
335 } else {
336 Log.w(TAG, root.toString() + " doesn't provide available bytes.");
337 }
338 }
339
340 if (!result) {
341 failedFileCount += mSrcs.size();
342 failedFiles.addAll(mSrcs);
343 }
344
345 return result;
346 }
347
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900348 @Override
349 boolean hasWarnings() {
350 return !convertedFiles.isEmpty();
351 }
352
Steve McKayc83baa02016-01-06 18:32:13 -0800353 /**
354 * Logs progress on the current copy operation. Displays/Updates the progress notification.
355 *
356 * @param bytesCopied
357 */
358 private void makeCopyProgress(long bytesCopied) {
359 onBytesCopied(bytesCopied);
Steve McKayc83baa02016-01-06 18:32:13 -0800360 }
361
362 /**
363 * Copies a the given document to the given location.
364 *
Steve McKay97b4be42016-01-20 15:09:35 -0800365 * @param src DocumentInfos for the documents to copy.
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +0900366 * @param srcParent DocumentInfo for the parent of the document to process.
Steve McKayc83baa02016-01-06 18:32:13 -0800367 * @param dstDirInfo The destination directory.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900368 * @throws ResourceException
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +0900369 *
370 * TODO: Stop passing srcParent, as it's not used for copy, but for move only.
Steve McKayc83baa02016-01-06 18:32:13 -0800371 */
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900372 void processDocument(DocumentInfo src, DocumentInfo srcParent,
373 DocumentInfo dstDirInfo) throws ResourceException {
Steve McKayc83baa02016-01-06 18:32:13 -0800374
375 // TODO: When optimized copy kicks in, we'll not making any progress updates.
376 // For now. Local storage isn't using optimized copy.
377
Tomasz Mikolajewski4aa89672016-01-21 10:00:33 +0900378 // When copying within the same provider, try to use optimized copying.
Steve McKayc83baa02016-01-06 18:32:13 -0800379 // If not supported, then fallback to byte-by-byte copy/move.
Steve McKay97b4be42016-01-20 15:09:35 -0800380 if (src.authority.equals(dstDirInfo.authority)) {
381 if ((src.flags & Document.FLAG_SUPPORTS_COPY) != 0) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900382 try {
383 if (DocumentsContract.copyDocument(getClient(src), src.derivedUri,
Tomasz Mikolajewskife7f5362016-03-02 17:41:40 +0900384 dstDirInfo.derivedUri) != null) {
385 return;
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900386 }
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900387 } catch (RemoteException | RuntimeException e) {
Tomasz Mikolajewskife7f5362016-03-02 17:41:40 +0900388 Log.e(TAG, "Provider side copy failed for: " + src.derivedUri
Steve McKayb1b08b22016-06-14 15:56:50 -0700389 + " due to an exception.", e);
Steve McKayc83baa02016-01-06 18:32:13 -0800390 }
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700391
Tomasz Mikolajewskife7f5362016-03-02 17:41:40 +0900392 // If optimized copy fails, then fallback to byte-by-byte copy.
393 if (DEBUG) Log.d(TAG, "Fallback to byte-by-byte copy for: " + src.derivedUri);
Steve McKayc83baa02016-01-06 18:32:13 -0800394 }
395 }
396
397 // If we couldn't do an optimized copy...we fall back to vanilla byte copy.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900398 byteCopyDocument(src, dstDirInfo);
Steve McKayc83baa02016-01-06 18:32:13 -0800399 }
400
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900401 void byteCopyDocument(DocumentInfo src, DocumentInfo dest) throws ResourceException {
Steve McKayc83baa02016-01-06 18:32:13 -0800402 final String dstMimeType;
403 final String dstDisplayName;
404
Steve McKay97b4be42016-01-20 15:09:35 -0800405 if (DEBUG) Log.d(TAG, "Doing byte copy of document: " + src);
Steve McKayc83baa02016-01-06 18:32:13 -0800406 // If the file is virtual, but can be converted to another format, then try to copy it
407 // as such format. Also, append an extension for the target mime type (if known).
Steve McKay358c3ec2016-10-21 09:16:57 -0700408 if (src.isVirtual()) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900409 String[] streamTypes = null;
410 try {
411 streamTypes = getContentResolver().getStreamTypes(src.derivedUri, "*/*");
412 } catch (RuntimeException e) {
413 throw new ResourceException(
414 "Failed to obtain streamable types for %s due to an exception.",
415 src.derivedUri, e);
416 }
Steve McKayc83baa02016-01-06 18:32:13 -0800417 if (streamTypes != null && streamTypes.length > 0) {
418 dstMimeType = streamTypes[0];
419 final String extension = MimeTypeMap.getSingleton().
420 getExtensionFromMimeType(dstMimeType);
Steve McKay97b4be42016-01-20 15:09:35 -0800421 dstDisplayName = src.displayName +
422 (extension != null ? "." + extension : src.displayName);
Steve McKayc83baa02016-01-06 18:32:13 -0800423 } else {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900424 throw new ResourceException("Cannot copy virtual file %s. No streamable formats "
425 + "available.", src.derivedUri);
Steve McKayc83baa02016-01-06 18:32:13 -0800426 }
427 } else {
Steve McKay97b4be42016-01-20 15:09:35 -0800428 dstMimeType = src.mimeType;
429 dstDisplayName = src.displayName;
Steve McKayc83baa02016-01-06 18:32:13 -0800430 }
431
432 // Create the target document (either a file or a directory), then copy recursively the
433 // contents (bytes or children).
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900434 Uri dstUri = null;
435 try {
436 dstUri = DocumentsContract.createDocument(
437 getClient(dest), dest.derivedUri, dstMimeType, dstDisplayName);
438 } catch (RemoteException | RuntimeException e) {
439 throw new ResourceException(
440 "Couldn't create destination document " + dstDisplayName + " in directory %s "
441 + "due to an exception.", dest.derivedUri, e);
442 }
Steve McKayc83baa02016-01-06 18:32:13 -0800443 if (dstUri == null) {
444 // If this is a directory, the entire subdir will not be copied over.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900445 throw new ResourceException(
446 "Couldn't create destination document " + dstDisplayName + " in directory %s.",
447 dest.derivedUri);
Steve McKayc83baa02016-01-06 18:32:13 -0800448 }
449
450 DocumentInfo dstInfo = null;
451 try {
452 dstInfo = DocumentInfo.fromUri(getContentResolver(), dstUri);
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900453 } catch (FileNotFoundException | RuntimeException e) {
454 throw new ResourceException("Could not load DocumentInfo for newly created file %s.",
455 dstUri);
Steve McKayc83baa02016-01-06 18:32:13 -0800456 }
457
Steve McKay97b4be42016-01-20 15:09:35 -0800458 if (Document.MIME_TYPE_DIR.equals(src.mimeType)) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900459 copyDirectoryHelper(src, dstInfo);
Steve McKayc83baa02016-01-06 18:32:13 -0800460 } else {
Tomasz Mikolajewski8fe54982016-02-23 15:12:54 +0900461 copyFileHelper(src, dstInfo, dest, dstMimeType);
Steve McKayc83baa02016-01-06 18:32:13 -0800462 }
Steve McKayc83baa02016-01-06 18:32:13 -0800463 }
464
465 /**
466 * Handles recursion into a directory and copying its contents. Note that in linux terms, this
467 * does the equivalent of "cp src/* dst", not "cp -r src dst".
468 *
Steve McKay97b4be42016-01-20 15:09:35 -0800469 * @param srcDir Info of the directory to copy from. The routine will copy the directory's
Steve McKayc83baa02016-01-06 18:32:13 -0800470 * contents, not the directory itself.
Steve McKay97b4be42016-01-20 15:09:35 -0800471 * @param destDir Info of the directory to copy to. Must be created beforehand.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900472 * @throws ResourceException
Steve McKayc83baa02016-01-06 18:32:13 -0800473 */
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900474 private void copyDirectoryHelper(DocumentInfo srcDir, DocumentInfo destDir)
475 throws ResourceException {
Steve McKayc83baa02016-01-06 18:32:13 -0800476 // Recurse into directories. Copy children into the new subdirectory.
477 final String queryColumns[] = new String[] {
478 Document.COLUMN_DISPLAY_NAME,
479 Document.COLUMN_DOCUMENT_ID,
480 Document.COLUMN_MIME_TYPE,
481 Document.COLUMN_SIZE,
482 Document.COLUMN_FLAGS
483 };
484 Cursor cursor = null;
485 boolean success = true;
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900486 // Iterate over srcs in the directory; copy to the destination directory.
487 final Uri queryUri = buildChildDocumentsUri(srcDir.authority, srcDir.documentId);
Steve McKayc83baa02016-01-06 18:32:13 -0800488 try {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900489 try {
490 cursor = getClient(srcDir).query(queryUri, queryColumns, null, null, null);
491 } catch (RemoteException | RuntimeException e) {
492 throw new ResourceException("Failed to query children of %s due to an exception.",
493 srcDir.derivedUri, e);
Steve McKayc83baa02016-01-06 18:32:13 -0800494 }
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900495
496 DocumentInfo src;
497 while (cursor.moveToNext() && !isCanceled()) {
498 try {
499 src = DocumentInfo.fromCursor(cursor, srcDir.authority);
500 processDocument(src, srcDir, destDir);
501 } catch (RuntimeException e) {
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700502 Log.e(TAG, String.format(
503 "Failed to recursively process a file %s due to an exception.",
504 srcDir.derivedUri.toString()), e);
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900505 success = false;
506 }
507 }
508 } catch (RuntimeException e) {
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700509 Log.e(TAG, String.format(
510 "Failed to copy a file %s to %s. ",
511 srcDir.derivedUri.toString(), destDir.derivedUri.toString()), e);
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900512 success = false;
Steve McKayc83baa02016-01-06 18:32:13 -0800513 } finally {
514 IoUtils.closeQuietly(cursor);
515 }
516
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900517 if (!success) {
518 throw new RuntimeException("Some files failed to copy during a recursive "
519 + "directory copy.");
520 }
Steve McKayc83baa02016-01-06 18:32:13 -0800521 }
522
523 /**
524 * Handles copying a single file.
525 *
Tomasz Mikolajewski8fe54982016-02-23 15:12:54 +0900526 * @param src Info of the file to copy from.
527 * @param dest Info of the *file* to copy to. Must be created beforehand.
528 * @param destParent Info of the parent of the destination.
Steve McKayc83baa02016-01-06 18:32:13 -0800529 * @param mimeType Mime type for the target. Can be different than source for virtual files.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900530 * @throws ResourceException
Steve McKayc83baa02016-01-06 18:32:13 -0800531 */
Tomasz Mikolajewski8fe54982016-02-23 15:12:54 +0900532 private void copyFileHelper(DocumentInfo src, DocumentInfo dest, DocumentInfo destParent,
533 String mimeType) throws ResourceException {
Steve McKayc83baa02016-01-06 18:32:13 -0800534 CancellationSignal canceller = new CancellationSignal();
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900535 AssetFileDescriptor srcFileAsAsset = null;
Steve McKayc83baa02016-01-06 18:32:13 -0800536 ParcelFileDescriptor srcFile = null;
537 ParcelFileDescriptor dstFile = null;
Steve McKay97b4be42016-01-20 15:09:35 -0800538 InputStream in = null;
Daichi Hirono75512402016-03-28 16:07:45 +0900539 ParcelFileDescriptor.AutoCloseOutputStream out = null;
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900540 boolean success = false;
Steve McKayc83baa02016-01-06 18:32:13 -0800541
Steve McKayc83baa02016-01-06 18:32:13 -0800542 try {
543 // If the file is virtual, but can be converted to another format, then try to copy it
544 // as such format.
Steve McKay358c3ec2016-10-21 09:16:57 -0700545 if (src.isVirtual()) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900546 try {
547 srcFileAsAsset = getClient(src).openTypedAssetFileDescriptor(
Steve McKay97b4be42016-01-20 15:09:35 -0800548 src.derivedUri, mimeType, null, canceller);
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900549 } catch (FileNotFoundException | RemoteException | RuntimeException e) {
550 throw new ResourceException("Failed to open a file as asset for %s due to an "
551 + "exception.", src.derivedUri, e);
552 }
Steve McKayc83baa02016-01-06 18:32:13 -0800553 srcFile = srcFileAsAsset.getParcelFileDescriptor();
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900554 try {
555 in = new AssetFileDescriptor.AutoCloseInputStream(srcFileAsAsset);
556 } catch (IOException e) {
557 throw new ResourceException("Failed to open a file input stream for %s due "
558 + "an exception.", src.derivedUri, e);
559 }
Steve McKayc83baa02016-01-06 18:32:13 -0800560 } else {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900561 try {
562 srcFile = getClient(src).openFile(src.derivedUri, "r", canceller);
563 } catch (FileNotFoundException | RemoteException | RuntimeException e) {
564 throw new ResourceException(
565 "Failed to open a file for %s due to an exception.", src.derivedUri, e);
566 }
Steve McKay97b4be42016-01-20 15:09:35 -0800567 in = new ParcelFileDescriptor.AutoCloseInputStream(srcFile);
Steve McKayc83baa02016-01-06 18:32:13 -0800568 }
569
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900570 try {
571 dstFile = getClient(dest).openFile(dest.derivedUri, "w", canceller);
572 } catch (FileNotFoundException | RemoteException | RuntimeException e) {
573 throw new ResourceException("Failed to open the destination file %s for writing "
574 + "due to an exception.", dest.derivedUri, e);
575 }
Steve McKay97b4be42016-01-20 15:09:35 -0800576 out = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);
Steve McKayc83baa02016-01-06 18:32:13 -0800577
Steve McKaybbeba522016-01-13 17:17:39 -0800578 byte[] buffer = new byte[32 * 1024];
Steve McKayc83baa02016-01-06 18:32:13 -0800579 int len;
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900580 try {
581 while ((len = in.read(buffer)) != -1) {
582 if (isCanceled()) {
Steve McKay003097d2016-02-23 10:06:50 -0800583 if (DEBUG) Log.d(TAG, "Canceled copy mid-copy of: " + src.derivedUri);
584 return;
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900585 }
586 out.write(buffer, 0, len);
587 makeCopyProgress(len);
Steve McKayc83baa02016-01-06 18:32:13 -0800588 }
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900589
Daichi Hirono0f3b4bd2016-11-16 15:42:42 +0900590 // Need to invoke Os#fsync to ensure the file is written to the storage device.
591 try {
592 Os.fsync(dstFile.getFileDescriptor());
593 } catch (ErrnoException error) {
594 // fsync will fail with fd of pipes and return EROFS or EINVAL.
595 if (error.errno != OsConstants.EROFS && error.errno != OsConstants.EINVAL) {
596 throw new SyncFailedException(
597 "Failed to sync bytes after copying a file.");
598 }
599 }
600
Daichi Hirono75512402016-03-28 16:07:45 +0900601 // Need to invoke IoUtils.close explicitly to avoid from ignoring errors at flush.
602 IoUtils.close(dstFile.getFileDescriptor());
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900603 srcFile.checkError();
604 } catch (IOException e) {
605 throw new ResourceException(
606 "Failed to copy bytes from %s to %s due to an IO exception.",
607 src.derivedUri, dest.derivedUri, e);
Steve McKayc83baa02016-01-06 18:32:13 -0800608 }
609
Steve McKay358c3ec2016-10-21 09:16:57 -0700610 if (src.isVirtual()) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900611 convertedFiles.add(src);
Steve McKayc83baa02016-01-06 18:32:13 -0800612 }
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900613
614 success = true;
Steve McKayc83baa02016-01-06 18:32:13 -0800615 } finally {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900616 if (!success) {
617 if (dstFile != null) {
618 try {
619 dstFile.closeWithError("Error copying bytes.");
620 } catch (IOException closeError) {
621 Log.w(TAG, "Error closing destination.", closeError);
622 }
623 }
624
625 if (DEBUG) Log.d(TAG, "Cleaning up failed operation leftovers.");
626 canceller.cancel();
627 try {
Tomasz Mikolajewski8fe54982016-02-23 15:12:54 +0900628 deleteDocument(dest, destParent);
629 } catch (ResourceException e) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900630 Log.w(TAG, "Failed to cleanup after copy error: " + src.derivedUri, e);
631 }
632 }
633
Steve McKayc83baa02016-01-06 18:32:13 -0800634 // This also ensures the file descriptors are closed.
Steve McKay97b4be42016-01-20 15:09:35 -0800635 IoUtils.closeQuietly(in);
636 IoUtils.closeQuietly(out);
Steve McKayc83baa02016-01-06 18:32:13 -0800637 }
Steve McKayc83baa02016-01-06 18:32:13 -0800638 }
639
640 /**
641 * Calculates the cumulative size of all the documents in the list. Directories are recursed
642 * into and totaled up.
643 *
644 * @param srcs
645 * @return Size in bytes.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900646 * @throws ResourceException
Steve McKayc83baa02016-01-06 18:32:13 -0800647 */
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900648 private long calculateSize(List<DocumentInfo> srcs) throws ResourceException {
Steve McKayc83baa02016-01-06 18:32:13 -0800649 long result = 0;
650
651 for (DocumentInfo src : srcs) {
652 if (src.isDirectory()) {
653 // Directories need to be recursed into.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900654 try {
655 result += calculateFileSizesRecursively(getClient(src), src.derivedUri);
656 } catch (RemoteException e) {
657 throw new ResourceException("Failed to obtain the client for %s.",
Garfield, Tanc20c6f62016-07-06 17:24:20 -0700658 src.derivedUri, e);
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900659 }
Steve McKayc83baa02016-01-06 18:32:13 -0800660 } else {
661 result += src.size;
662 }
Garfield, Tanedce5542016-06-17 15:32:28 -0700663
664 if (isCanceled()) {
665 return result;
666 }
Steve McKayc83baa02016-01-06 18:32:13 -0800667 }
668 return result;
669 }
670
671 /**
672 * Calculates (recursively) the cumulative size of all the files under the given directory.
673 *
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900674 * @throws ResourceException
Steve McKayc83baa02016-01-06 18:32:13 -0800675 */
Garfield, Tanc20c6f62016-07-06 17:24:20 -0700676 long calculateFileSizesRecursively(
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900677 ContentProviderClient client, Uri uri) throws ResourceException {
Steve McKayc83baa02016-01-06 18:32:13 -0800678 final String authority = uri.getAuthority();
Steve McKaybbeba522016-01-13 17:17:39 -0800679 final Uri queryUri = buildChildDocumentsUri(authority, getDocumentId(uri));
Steve McKayc83baa02016-01-06 18:32:13 -0800680 final String queryColumns[] = new String[] {
681 Document.COLUMN_DOCUMENT_ID,
682 Document.COLUMN_MIME_TYPE,
683 Document.COLUMN_SIZE
684 };
685
686 long result = 0;
687 Cursor cursor = null;
688 try {
689 cursor = client.query(queryUri, queryColumns, null, null, null);
Garfield, Tanedce5542016-06-17 15:32:28 -0700690 while (cursor.moveToNext() && !isCanceled()) {
Steve McKayc83baa02016-01-06 18:32:13 -0800691 if (Document.MIME_TYPE_DIR.equals(
692 getCursorString(cursor, Document.COLUMN_MIME_TYPE))) {
693 // Recurse into directories.
Steve McKaybbeba522016-01-13 17:17:39 -0800694 final Uri dirUri = buildDocumentUri(authority,
Steve McKayc83baa02016-01-06 18:32:13 -0800695 getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
696 result += calculateFileSizesRecursively(client, dirUri);
697 } else {
698 // This may return -1 if the size isn't defined. Ignore those cases.
699 long size = getCursorLong(cursor, Document.COLUMN_SIZE);
700 result += size > 0 ? size : 0;
701 }
702 }
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900703 } catch (RemoteException | RuntimeException e) {
704 throw new ResourceException(
705 "Failed to calculate size for %s due to an exception.", uri, e);
Steve McKayc83baa02016-01-06 18:32:13 -0800706 } finally {
707 IoUtils.closeQuietly(cursor);
708 }
709
710 return result;
711 }
712
Steve McKayc83baa02016-01-06 18:32:13 -0800713 /**
714 * Returns true if {@code doc} is a descendant of {@code parentDoc}.
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900715 * @throws ResourceException
Steve McKayc83baa02016-01-06 18:32:13 -0800716 */
Steve McKay97b4be42016-01-20 15:09:35 -0800717 boolean isDescendentOf(DocumentInfo doc, DocumentInfo parent)
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900718 throws ResourceException {
Steve McKay97b4be42016-01-20 15:09:35 -0800719 if (parent.isDirectory() && doc.authority.equals(parent.authority)) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900720 try {
721 return isChildDocument(getClient(doc), doc.derivedUri, parent.derivedUri);
722 } catch (RemoteException | RuntimeException e) {
723 throw new ResourceException(
724 "Failed to check if %s is a child of %s due to an exception.",
725 doc.derivedUri, parent.derivedUri, e);
726 }
Steve McKayc83baa02016-01-06 18:32:13 -0800727 }
728 return false;
729 }
Steve McKaybbeba522016-01-13 17:17:39 -0800730
Steve McKay97b4be42016-01-20 15:09:35 -0800731 @Override
732 public String toString() {
733 return new StringBuilder()
734 .append("CopyJob")
735 .append("{")
736 .append("id=" + id)
Garfield, Tan48334772016-06-28 17:17:38 -0700737 .append(", docs=" + srcs)
Steve McKay97b4be42016-01-20 15:09:35 -0800738 .append(", destination=" + stack)
739 .append("}")
740 .toString();
741 }
Steve McKaybbeba522016-01-13 17:17:39 -0800742}