blob: 1c97b85a23e897d188c89d0d503ff707203282b9 [file] [log] [blame]
Ben Kwa41b26c12015-03-31 10:11:43 -07001/*
2 * Copyright (C) 2015 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;
18
Ben Kwaf527c632015-04-08 15:03:35 -070019import static com.android.documentsui.model.DocumentInfo.getCursorLong;
20import static com.android.documentsui.model.DocumentInfo.getCursorString;
21
Ben Kwa41b26c12015-03-31 10:11:43 -070022import android.app.IntentService;
23import android.app.Notification;
24import android.app.NotificationManager;
25import android.app.PendingIntent;
Ben Kwaf527c632015-04-08 15:03:35 -070026import android.content.ContentProviderClient;
Ben Kwa41b26c12015-03-31 10:11:43 -070027import android.content.Context;
28import android.content.Intent;
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +090029import android.content.res.Resources;
Ben Kwaf527c632015-04-08 15:03:35 -070030import android.database.Cursor;
Ben Kwa41b26c12015-03-31 10:11:43 -070031import android.net.Uri;
Ben Kwaf5858932015-04-07 15:43:39 -070032import android.os.CancellationSignal;
Ben Kwaf5858932015-04-07 15:43:39 -070033import android.os.ParcelFileDescriptor;
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +090034import android.os.Parcelable;
Ben Kwaf527c632015-04-08 15:03:35 -070035import android.os.RemoteException;
Ben Kwa41b26c12015-03-31 10:11:43 -070036import android.os.SystemClock;
Ben Kwaf5858932015-04-07 15:43:39 -070037import android.provider.DocumentsContract;
Ben Kwaf527c632015-04-08 15:03:35 -070038import android.provider.DocumentsContract.Document;
Ben Kwa41b26c12015-03-31 10:11:43 -070039import android.text.format.DateUtils;
40import android.util.Log;
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +090041import android.widget.Toast;
Ben Kwa41b26c12015-03-31 10:11:43 -070042
43import com.android.documentsui.model.DocumentInfo;
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +090044import com.android.documentsui.model.DocumentStack;
Ben Kwa41b26c12015-03-31 10:11:43 -070045
46import libcore.io.IoUtils;
47
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +090048import java.io.FileNotFoundException;
Ben Kwa41b26c12015-03-31 10:11:43 -070049import java.io.IOException;
50import java.io.InputStream;
51import java.io.OutputStream;
52import java.text.NumberFormat;
53import java.util.ArrayList;
Ben Kwaf527c632015-04-08 15:03:35 -070054import java.util.List;
55import java.util.Objects;
Ben Kwa41b26c12015-03-31 10:11:43 -070056
57public class CopyService extends IntentService {
58 public static final String TAG = "CopyService";
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +090059
Ben Kwa41b26c12015-03-31 10:11:43 -070060 private static final String EXTRA_CANCEL = "com.android.documentsui.CANCEL";
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +090061 public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST";
62 public static final String EXTRA_STACK = "com.android.documentsui.STACK";
Tomasz Mikolajewski332d8192015-04-13 19:38:43 +090063 public static final String EXTRA_FAILURE = "com.android.documentsui.FAILURE";
Ben Kwacb4461f2015-05-05 11:50:11 -070064 public static final String EXTRA_TRANSFER_MODE = "com.android.documentsui.TRANSFER_MODE";
65
66 public static final int TRANSFER_MODE_NONE = 0;
67 public static final int TRANSFER_MODE_COPY = 1;
68 public static final int TRANSFER_MODE_MOVE = 2;
Tomasz Mikolajewski332d8192015-04-13 19:38:43 +090069
70 // TODO: Move it to a shared file when more operations are implemented.
71 public static final int FAILURE_COPY = 1;
Ben Kwa41b26c12015-03-31 10:11:43 -070072
73 private NotificationManager mNotificationManager;
74 private Notification.Builder mProgressBuilder;
75
76 // Jobs are serialized but a job ID is used, to avoid mixing up cancellation requests.
77 private String mJobId;
78 private volatile boolean mIsCancelled;
79 // Parameters of the copy job. Requests to an IntentService are serialized so this code only
80 // needs to deal with one job at a time.
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +090081 private final ArrayList<DocumentInfo> mFailedFiles;
Ben Kwa41b26c12015-03-31 10:11:43 -070082 private long mBatchSize;
83 private long mBytesCopied;
84 private long mStartTime;
85 private long mLastNotificationTime;
86 // Speed estimation
87 private long mBytesCopiedSample;
88 private long mSampleTime;
89 private long mSpeed;
90 private long mRemainingTime;
Ben Kwaf527c632015-04-08 15:03:35 -070091 // Provider clients are acquired for the duration of each copy job. Note that there is an
92 // implicit assumption that all srcs come from the same authority.
93 private ContentProviderClient mSrcClient;
94 private ContentProviderClient mDstClient;
Ben Kwa41b26c12015-03-31 10:11:43 -070095
96 public CopyService() {
97 super("CopyService");
Ben Kwaf527c632015-04-08 15:03:35 -070098
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +090099 mFailedFiles = new ArrayList<DocumentInfo>();
100 }
101
102 /**
103 * Starts the service for a copy operation.
104 *
105 * @param context Context for the intent.
106 * @param srcDocs A list of src files to copy.
107 * @param dstStack The copy destination stack.
108 */
Ben Kwacb4461f2015-05-05 11:50:11 -0700109 public static void start(Context context, List<DocumentInfo> srcDocs, DocumentStack dstStack,
110 int mode) {
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +0900111 final Resources res = context.getResources();
112 final Intent copyIntent = new Intent(context, CopyService.class);
113 copyIntent.putParcelableArrayListExtra(
114 EXTRA_SRC_LIST, new ArrayList<DocumentInfo>(srcDocs));
115 copyIntent.putExtra(EXTRA_STACK, (Parcelable) dstStack);
Ben Kwacb4461f2015-05-05 11:50:11 -0700116 copyIntent.putExtra(EXTRA_TRANSFER_MODE, mode);
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +0900117
Ben Kwacb4461f2015-05-05 11:50:11 -0700118 int toastMessage = (mode == TRANSFER_MODE_COPY) ? R.plurals.copy_begin
119 : R.plurals.move_begin;
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +0900120 Toast.makeText(context,
Ben Kwacb4461f2015-05-05 11:50:11 -0700121 res.getQuantityString(toastMessage, srcDocs.size(), srcDocs.size()),
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +0900122 Toast.LENGTH_SHORT).show();
123 context.startService(copyIntent);
Ben Kwa41b26c12015-03-31 10:11:43 -0700124 }
125
126 @Override
127 public int onStartCommand(Intent intent, int flags, int startId) {
128 if (intent.hasExtra(EXTRA_CANCEL)) {
129 handleCancel(intent);
130 }
131 return super.onStartCommand(intent, flags, startId);
132 }
133
134 @Override
135 protected void onHandleIntent(Intent intent) {
136 if (intent.hasExtra(EXTRA_CANCEL)) {
137 handleCancel(intent);
138 return;
139 }
140
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +0900141 final ArrayList<DocumentInfo> srcs = intent.getParcelableArrayListExtra(EXTRA_SRC_LIST);
142 final DocumentStack stack = intent.getParcelableExtra(EXTRA_STACK);
Ben Kwacb4461f2015-05-05 11:50:11 -0700143 // Copy by default.
144 final int transferMode = intent.getIntExtra(EXTRA_TRANSFER_MODE, TRANSFER_MODE_COPY);
Ben Kwa41b26c12015-03-31 10:11:43 -0700145
Ben Kwaf527c632015-04-08 15:03:35 -0700146 try {
147 // Acquire content providers.
148 mSrcClient = DocumentsApplication.acquireUnstableProviderOrThrow(getContentResolver(),
149 srcs.get(0).authority);
150 mDstClient = DocumentsApplication.acquireUnstableProviderOrThrow(getContentResolver(),
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +0900151 stack.peek().authority);
Ben Kwa41b26c12015-03-31 10:11:43 -0700152
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +0900153 setupCopyJob(srcs, stack);
Ben Kwaf527c632015-04-08 15:03:35 -0700154
155 for (int i = 0; i < srcs.size() && !mIsCancelled; ++i) {
Ben Kwacb4461f2015-05-05 11:50:11 -0700156 copy(srcs.get(i), stack.peek(), transferMode);
Ben Kwa41b26c12015-03-31 10:11:43 -0700157 }
Ben Kwaf527c632015-04-08 15:03:35 -0700158 } catch (Exception e) {
159 // Catch-all to prevent any copy errors from wedging the app.
160 Log.e(TAG, "Exceptions occurred during copying", e);
161 } finally {
162 ContentProviderClient.releaseQuietly(mSrcClient);
163 ContentProviderClient.releaseQuietly(mDstClient);
164
165 // Dismiss the ongoing copy notification when the copy is done.
166 mNotificationManager.cancel(mJobId, 0);
167
168 if (mFailedFiles.size() > 0) {
Tomasz Mikolajewski332d8192015-04-13 19:38:43 +0900169 final Context context = getApplicationContext();
170 final Intent navigateIntent = new Intent(context, StandaloneActivity.class);
171 navigateIntent.putExtra(EXTRA_STACK, (Parcelable) stack);
172 navigateIntent.putExtra(EXTRA_FAILURE, FAILURE_COPY);
173 navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, mFailedFiles);
174
175 final Notification.Builder errorBuilder = new Notification.Builder(this)
176 .setContentTitle(context.getResources().
177 getQuantityString(R.plurals.copy_error_notification_title,
178 mFailedFiles.size(), mFailedFiles.size()))
179 .setContentText(getString(R.string.notification_touch_for_details))
180 .setContentIntent(PendingIntent.getActivity(context, 0, navigateIntent,
181 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
182 .setCategory(Notification.CATEGORY_ERROR)
183 .setSmallIcon(R.drawable.ic_menu_copy)
184 .setAutoCancel(true);
185 mNotificationManager.notify(mJobId, 0, errorBuilder.build());
Ben Kwaf527c632015-04-08 15:03:35 -0700186 }
Ben Kwa41b26c12015-03-31 10:11:43 -0700187 }
Ben Kwa41b26c12015-03-31 10:11:43 -0700188 }
189
190 @Override
191 public void onCreate() {
192 super.onCreate();
193 mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
194 }
195
196 /**
197 * Sets up the CopyService to start tracking and sending notifications for the given batch of
198 * files.
199 *
200 * @param srcs A list of src files to copy.
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +0900201 * @param stack The copy destination stack.
Ben Kwaf527c632015-04-08 15:03:35 -0700202 * @throws RemoteException
Ben Kwa41b26c12015-03-31 10:11:43 -0700203 */
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +0900204 private void setupCopyJob(ArrayList<DocumentInfo> srcs, DocumentStack stack)
Ben Kwaf527c632015-04-08 15:03:35 -0700205 throws RemoteException {
Ben Kwa41b26c12015-03-31 10:11:43 -0700206 // Create an ID for this copy job. Use the timestamp.
207 mJobId = String.valueOf(SystemClock.elapsedRealtime());
208 // Reset the cancellation flag.
209 mIsCancelled = false;
210
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +0900211 final Context context = getApplicationContext();
212 final Intent navigateIntent = new Intent(context, StandaloneActivity.class);
Tomasz Mikolajewski332d8192015-04-13 19:38:43 +0900213 navigateIntent.putExtra(EXTRA_STACK, (Parcelable) stack);
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +0900214
Ben Kwa41b26c12015-03-31 10:11:43 -0700215 mProgressBuilder = new Notification.Builder(this)
216 .setContentTitle(getString(R.string.copy_notification_title))
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +0900217 .setContentIntent(PendingIntent.getActivity(context, 0, navigateIntent, 0))
Ben Kwa41b26c12015-03-31 10:11:43 -0700218 .setCategory(Notification.CATEGORY_PROGRESS)
Tomasz Mikolajewski332d8192015-04-13 19:38:43 +0900219 .setSmallIcon(R.drawable.ic_menu_copy)
220 .setOngoing(true);
Ben Kwa41b26c12015-03-31 10:11:43 -0700221
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +0900222 final Intent cancelIntent = new Intent(this, CopyService.class);
Ben Kwa41b26c12015-03-31 10:11:43 -0700223 cancelIntent.putExtra(EXTRA_CANCEL, mJobId);
224 mProgressBuilder.addAction(R.drawable.ic_cab_cancel,
Daichi Hirono677ce6c2015-04-15 14:33:52 +0900225 getString(android.R.string.cancel), PendingIntent.getService(this, 0,
Ben Kwac89eb7c2015-04-15 12:13:32 -0700226 cancelIntent,
227 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT));
Ben Kwa41b26c12015-03-31 10:11:43 -0700228
Ben Kwa41b26c12015-03-31 10:11:43 -0700229 // Send an initial progress notification.
Ben Kwaf527c632015-04-08 15:03:35 -0700230 mProgressBuilder.setProgress(0, 0, true); // Indeterminate progress while setting up.
231 mProgressBuilder.setContentText(getString(R.string.copy_preparing));
Ben Kwa41b26c12015-03-31 10:11:43 -0700232 mNotificationManager.notify(mJobId, 0, mProgressBuilder.build());
233
234 // Reset batch parameters.
Ben Kwaf527c632015-04-08 15:03:35 -0700235 mFailedFiles.clear();
236 mBatchSize = calculateFileSizes(srcs);
Ben Kwa41b26c12015-03-31 10:11:43 -0700237 mBytesCopied = 0;
238 mStartTime = SystemClock.elapsedRealtime();
239 mLastNotificationTime = 0;
240 mBytesCopiedSample = 0;
241 mSampleTime = 0;
242 mSpeed = 0;
243 mRemainingTime = 0;
244
245 // TODO: Check preconditions for copy.
246 // - check that the destination has enough space and is writeable?
247 // - check MIME types?
248 }
249
250 /**
Ben Kwaf527c632015-04-08 15:03:35 -0700251 * Calculates the cumulative size of all the documents in the list. Directories are recursed
252 * into and totaled up.
253 *
254 * @param srcs
255 * @return Size in bytes.
256 * @throws RemoteException
257 */
258 private long calculateFileSizes(List<DocumentInfo> srcs) throws RemoteException {
259 long result = 0;
260 for (DocumentInfo src : srcs) {
261 if (Document.MIME_TYPE_DIR.equals(src.mimeType)) {
262 // Directories need to be recursed into.
263 result += calculateFileSizesHelper(src.derivedUri);
264 } else {
265 result += src.size;
266 }
267 }
268 return result;
269 }
270
271 /**
272 * Calculates (recursively) the cumulative size of all the files under the given directory.
273 *
274 * @throws RemoteException
275 */
276 private long calculateFileSizesHelper(Uri uri) throws RemoteException {
277 final String authority = uri.getAuthority();
278 final Uri queryUri = DocumentsContract.buildChildDocumentsUri(authority,
279 DocumentsContract.getDocumentId(uri));
280 final String queryColumns[] = new String[] {
281 Document.COLUMN_DOCUMENT_ID,
282 Document.COLUMN_MIME_TYPE,
283 Document.COLUMN_SIZE
284 };
285
286 long result = 0;
287 Cursor cursor = null;
288 try {
289 cursor = mSrcClient.query(queryUri, queryColumns, null, null, null);
290 while (cursor.moveToNext()) {
291 if (Document.MIME_TYPE_DIR.equals(
292 getCursorString(cursor, Document.COLUMN_MIME_TYPE))) {
293 // Recurse into directories.
294 final Uri subdirUri = DocumentsContract.buildDocumentUri(authority,
295 getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
296 result += calculateFileSizesHelper(subdirUri);
297 } else {
298 // This may return -1 if the size isn't defined. Ignore those cases.
299 long size = getCursorLong(cursor, Document.COLUMN_SIZE);
300 result += size > 0 ? size : 0;
301 }
302 }
303 } finally {
304 IoUtils.closeQuietly(cursor);
305 }
306
307 return result;
308 }
309
310 /**
Ben Kwa41b26c12015-03-31 10:11:43 -0700311 * Cancels the current copy job, if its ID matches the given ID.
312 *
313 * @param intent The cancellation intent.
314 */
315 private void handleCancel(Intent intent) {
316 final String cancelledId = intent.getStringExtra(EXTRA_CANCEL);
317 // Do nothing if the cancelled ID doesn't match the current job ID. This prevents racey
318 // cancellation requests from affecting unrelated copy jobs.
Ben Kwaf527c632015-04-08 15:03:35 -0700319 if (Objects.equals(mJobId, cancelledId)) {
Ben Kwa41b26c12015-03-31 10:11:43 -0700320 // Set the cancel flag. This causes the copy loops to exit.
321 mIsCancelled = true;
322 // Dismiss the progress notification here rather than in the copy loop. This preserves
323 // interactivity for the user in case the copy loop is stalled.
324 mNotificationManager.cancel(mJobId, 0);
325 }
326 }
327
328 /**
329 * Logs progress on the current copy operation. Displays/Updates the progress notification.
330 *
331 * @param bytesCopied
332 */
333 private void makeProgress(long bytesCopied) {
334 mBytesCopied += bytesCopied;
335 double done = (double) mBytesCopied / mBatchSize;
336 String percent = NumberFormat.getPercentInstance().format(done);
337
338 // Update time estimate
339 long currentTime = SystemClock.elapsedRealtime();
340 long elapsedTime = currentTime - mStartTime;
341
342 // Send out progress notifications once a second.
343 if (currentTime - mLastNotificationTime > 1000) {
344 updateRemainingTimeEstimate(elapsedTime);
345 mProgressBuilder.setProgress(100, (int) (done * 100), false);
346 mProgressBuilder.setContentInfo(percent);
347 if (mRemainingTime > 0) {
348 mProgressBuilder.setContentText(getString(R.string.copy_remaining,
349 DateUtils.formatDuration(mRemainingTime)));
350 } else {
351 mProgressBuilder.setContentText(null);
352 }
353 mNotificationManager.notify(mJobId, 0, mProgressBuilder.build());
354 mLastNotificationTime = currentTime;
355 }
356 }
357
358 /**
359 * Generates an estimate of the remaining time in the copy.
360 *
361 * @param elapsedTime The time elapsed so far.
362 */
363 private void updateRemainingTimeEstimate(long elapsedTime) {
364 final long sampleDuration = elapsedTime - mSampleTime;
365 final long sampleSpeed = ((mBytesCopied - mBytesCopiedSample) * 1000) / sampleDuration;
366 if (mSpeed == 0) {
367 mSpeed = sampleSpeed;
368 } else {
369 mSpeed = ((3 * mSpeed) + sampleSpeed) / 4;
370 }
371
372 if (mSampleTime > 0 && mSpeed > 0) {
373 mRemainingTime = ((mBatchSize - mBytesCopied) * 1000) / mSpeed;
374 } else {
375 mRemainingTime = 0;
376 }
377
378 mSampleTime = elapsedTime;
379 mBytesCopiedSample = mBytesCopied;
380 }
381
382 /**
Ben Kwaf527c632015-04-08 15:03:35 -0700383 * Copies a the given documents to the given location.
Ben Kwa41b26c12015-03-31 10:11:43 -0700384 *
Ben Kwaf527c632015-04-08 15:03:35 -0700385 * @param srcInfo DocumentInfos for the documents to copy.
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +0900386 * @param dstDirInfo The destination directory.
Ben Kwaf527c632015-04-08 15:03:35 -0700387 * @throws RemoteException
Ben Kwa41b26c12015-03-31 10:11:43 -0700388 */
Ben Kwacb4461f2015-05-05 11:50:11 -0700389 private void copy(DocumentInfo srcInfo, DocumentInfo dstDirInfo, int mode)
390 throws RemoteException {
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +0900391 final Uri dstUri = DocumentsContract.createDocument(mDstClient, dstDirInfo.derivedUri,
Ben Kwaf5858932015-04-07 15:43:39 -0700392 srcInfo.mimeType, srcInfo.displayName);
Ben Kwaf527c632015-04-08 15:03:35 -0700393 if (dstUri == null) {
394 // If this is a directory, the entire subdir will not be copied over.
395 Log.e(TAG, "Error while copying " + srcInfo.displayName);
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +0900396 mFailedFiles.add(srcInfo);
Ben Kwaf527c632015-04-08 15:03:35 -0700397 return;
398 }
Ben Kwaf5858932015-04-07 15:43:39 -0700399
Ben Kwaf527c632015-04-08 15:03:35 -0700400 if (Document.MIME_TYPE_DIR.equals(srcInfo.mimeType)) {
Ben Kwacb4461f2015-05-05 11:50:11 -0700401 copyDirectoryHelper(srcInfo.derivedUri, dstUri, mode);
Ben Kwaf527c632015-04-08 15:03:35 -0700402 } else {
Ben Kwacb4461f2015-05-05 11:50:11 -0700403 copyFileHelper(srcInfo.derivedUri, dstUri, mode);
Ben Kwaf527c632015-04-08 15:03:35 -0700404 }
405 }
406
407 /**
408 * Handles recursion into a directory and copying its contents. Note that in linux terms, this
409 * does the equivalent of "cp src/* dst", not "cp -r src dst".
410 *
411 * @param srcDirUri URI of the directory to copy from. The routine will copy the directory's
412 * contents, not the directory itself.
413 * @param dstDirUri URI of the directory to copy to. Must be created beforehand.
414 * @throws RemoteException
415 */
Ben Kwacb4461f2015-05-05 11:50:11 -0700416 private void copyDirectoryHelper(Uri srcDirUri, Uri dstDirUri, int mode)
417 throws RemoteException {
Ben Kwaf527c632015-04-08 15:03:35 -0700418 // Recurse into directories. Copy children into the new subdirectory.
419 final String queryColumns[] = new String[] {
420 Document.COLUMN_DISPLAY_NAME,
421 Document.COLUMN_DOCUMENT_ID,
422 Document.COLUMN_MIME_TYPE,
423 Document.COLUMN_SIZE
424 };
425 final Uri queryUri = DocumentsContract.buildChildDocumentsUri(srcDirUri.getAuthority(),
426 DocumentsContract.getDocumentId(srcDirUri));
427 Cursor cursor = null;
428 try {
429 // Iterate over srcs in the directory; copy to the destination directory.
430 cursor = mSrcClient.query(queryUri, queryColumns, null, null, null);
431 while (cursor.moveToNext()) {
432 final String childMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
433 final Uri dstUri = DocumentsContract.createDocument(mDstClient, dstDirUri,
434 childMimeType, getCursorString(cursor, Document.COLUMN_DISPLAY_NAME));
435 final Uri childUri = DocumentsContract.buildDocumentUri(srcDirUri.getAuthority(),
436 getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
437 if (Document.MIME_TYPE_DIR.equals(childMimeType)) {
Ben Kwacb4461f2015-05-05 11:50:11 -0700438 copyDirectoryHelper(childUri, dstUri, mode);
Ben Kwaf527c632015-04-08 15:03:35 -0700439 } else {
Ben Kwacb4461f2015-05-05 11:50:11 -0700440 copyFileHelper(childUri, dstUri, mode);
441 }
442 }
443 if (mode == TRANSFER_MODE_MOVE) {
444 try {
445 DocumentsContract.deleteDocument(mSrcClient, srcDirUri);
446 } catch (RemoteException e) {
447 // RemoteExceptions usually signal that the connection is dead, so there's no
448 // point attempting to continue. Propagate the exception up so the copy job is
449 // cancelled.
450 Log.w(TAG, "Failed to clean up after move: " + srcDirUri, e);
451 throw e;
Ben Kwaf527c632015-04-08 15:03:35 -0700452 }
453 }
454 } finally {
455 IoUtils.closeQuietly(cursor);
456 }
457 }
458
459 /**
460 * Handles copying a single file.
461 *
462 * @param srcUri URI of the file to copy from.
463 * @param dstUri URI of the *file* to copy to. Must be created beforehand.
464 * @throws RemoteException
465 */
Ben Kwacb4461f2015-05-05 11:50:11 -0700466 private void copyFileHelper(Uri srcUri, Uri dstUri, int mode)
467 throws RemoteException {
Ben Kwaf527c632015-04-08 15:03:35 -0700468 // Copy an individual file.
Ben Kwaf5858932015-04-07 15:43:39 -0700469 CancellationSignal canceller = new CancellationSignal();
470 ParcelFileDescriptor srcFile = null;
471 ParcelFileDescriptor dstFile = null;
472 InputStream src = null;
473 OutputStream dst = null;
Ben Kwa41b26c12015-03-31 10:11:43 -0700474
Ben Kwa37c76592015-04-24 15:35:25 -0700475 IOException copyError = null;
Ben Kwa41b26c12015-03-31 10:11:43 -0700476 try {
Ben Kwaf527c632015-04-08 15:03:35 -0700477 srcFile = mSrcClient.openFile(srcUri, "r", canceller);
478 dstFile = mDstClient.openFile(dstUri, "w", canceller);
Ben Kwaf5858932015-04-07 15:43:39 -0700479 src = new ParcelFileDescriptor.AutoCloseInputStream(srcFile);
480 dst = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);
Ben Kwa41b26c12015-03-31 10:11:43 -0700481
482 byte[] buffer = new byte[8192];
483 int len;
Ben Kwaf5858932015-04-07 15:43:39 -0700484 while (!mIsCancelled && ((len = src.read(buffer)) != -1)) {
485 dst.write(buffer, 0, len);
Ben Kwa41b26c12015-03-31 10:11:43 -0700486 makeProgress(len);
487 }
Ben Kwa37c76592015-04-24 15:35:25 -0700488
Ben Kwaf5858932015-04-07 15:43:39 -0700489 srcFile.checkError();
Ben Kwa41b26c12015-03-31 10:11:43 -0700490 } catch (IOException e) {
Ben Kwa37c76592015-04-24 15:35:25 -0700491 copyError = e;
Ben Kwa2d88d492015-05-05 11:58:38 -0700492 try {
493 dstFile.closeWithError(copyError.getMessage());
494 } catch (IOException closeError) {
495 Log.e(TAG, "Error closing destination", closeError);
Ben Kwa37c76592015-04-24 15:35:25 -0700496 }
Ben Kwa2d88d492015-05-05 11:58:38 -0700497 } finally {
Ben Kwaf5858932015-04-07 15:43:39 -0700498 // This also ensures the file descriptors are closed.
499 IoUtils.closeQuietly(src);
500 IoUtils.closeQuietly(dst);
Ben Kwa41b26c12015-03-31 10:11:43 -0700501 }
502
Ben Kwa37c76592015-04-24 15:35:25 -0700503 if (copyError != null) {
504 // Log errors.
505 Log.e(TAG, "Error while copying " + srcUri.toString(), copyError);
506 try {
507 mFailedFiles.add(DocumentInfo.fromUri(getContentResolver(), srcUri));
508 } catch (FileNotFoundException ignore) {
509 Log.w(TAG, "Source file gone: " + srcUri, copyError);
Ben Kwacb4461f2015-05-05 11:50:11 -0700510 // The source file is gone.
Ben Kwa37c76592015-04-24 15:35:25 -0700511 }
512 }
513
514 if (copyError != null || mIsCancelled) {
Ben Kwa41b26c12015-03-31 10:11:43 -0700515 // Clean up half-copied files.
Ben Kwaf5858932015-04-07 15:43:39 -0700516 canceller.cancel();
Ben Kwaf527c632015-04-08 15:03:35 -0700517 try {
518 DocumentsContract.deleteDocument(mDstClient, dstUri);
519 } catch (RemoteException e) {
Ben Kwacb4461f2015-05-05 11:50:11 -0700520 Log.w(TAG, "Failed to clean up after copy error: " + dstUri, e);
Ben Kwaf527c632015-04-08 15:03:35 -0700521 // RemoteExceptions usually signal that the connection is dead, so there's no point
522 // attempting to continue. Propagate the exception up so the copy job is cancelled.
523 throw e;
Ben Kwa41b26c12015-03-31 10:11:43 -0700524 }
Ben Kwacb4461f2015-05-05 11:50:11 -0700525 } else if (mode == TRANSFER_MODE_MOVE) {
526 // Clean up src files after a successful move.
527 try {
528 DocumentsContract.deleteDocument(mSrcClient, srcUri);
529 } catch (RemoteException e) {
530 Log.w(TAG, "Failed to clean up after move: " + srcUri, e);
531 throw e;
532 }
Ben Kwa41b26c12015-03-31 10:11:43 -0700533 }
534 }
535}