blob: 362052c58d5359c668f98f366b7364096ffbc6c0 [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
Steve McKayf8a5e082015-09-23 17:21:40 -070019import static com.android.documentsui.Shared.DEBUG;
Ben Kwaf527c632015-04-08 15:03:35 -070020import static com.android.documentsui.model.DocumentInfo.getCursorLong;
21import static com.android.documentsui.model.DocumentInfo.getCursorString;
22
Ben Kwac4693342015-09-30 10:00:10 -070023import android.app.Activity;
Ben Kwa41b26c12015-03-31 10:11:43 -070024import android.app.IntentService;
25import android.app.Notification;
26import android.app.NotificationManager;
27import android.app.PendingIntent;
Ben Kwaf527c632015-04-08 15:03:35 -070028import android.content.ContentProviderClient;
Ben Kwa41b26c12015-03-31 10:11:43 -070029import android.content.Context;
30import android.content.Intent;
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +090031import android.content.res.Resources;
Ben Kwaf527c632015-04-08 15:03:35 -070032import android.database.Cursor;
Ben Kwa41b26c12015-03-31 10:11:43 -070033import android.net.Uri;
Ben Kwaf5858932015-04-07 15:43:39 -070034import android.os.CancellationSignal;
Ben Kwaf5858932015-04-07 15:43:39 -070035import android.os.ParcelFileDescriptor;
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +090036import android.os.Parcelable;
Ben Kwaf527c632015-04-08 15:03:35 -070037import android.os.RemoteException;
Ben Kwa41b26c12015-03-31 10:11:43 -070038import android.os.SystemClock;
Ben Kwaf5858932015-04-07 15:43:39 -070039import android.provider.DocumentsContract;
Ben Kwaf527c632015-04-08 15:03:35 -070040import android.provider.DocumentsContract.Document;
Ben Kwac4693342015-09-30 10:00:10 -070041import android.support.design.widget.Snackbar;
Ben Kwa41b26c12015-03-31 10:11:43 -070042import android.text.format.DateUtils;
43import android.util.Log;
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +090044import android.widget.Toast;
Ben Kwa41b26c12015-03-31 10:11:43 -070045
46import com.android.documentsui.model.DocumentInfo;
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +090047import com.android.documentsui.model.DocumentStack;
Ben Kwa41b26c12015-03-31 10:11:43 -070048
49import libcore.io.IoUtils;
50
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +090051import java.io.FileNotFoundException;
Ben Kwa41b26c12015-03-31 10:11:43 -070052import java.io.IOException;
53import java.io.InputStream;
54import java.io.OutputStream;
55import java.text.NumberFormat;
56import java.util.ArrayList;
Ben Kwaf527c632015-04-08 15:03:35 -070057import java.util.List;
58import java.util.Objects;
Ben Kwa41b26c12015-03-31 10:11:43 -070059
60public class CopyService extends IntentService {
61 public static final String TAG = "CopyService";
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +090062
Ben Kwa41b26c12015-03-31 10:11:43 -070063 private static final String EXTRA_CANCEL = "com.android.documentsui.CANCEL";
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +090064 public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST";
Tomasz Mikolajewski332d8192015-04-13 19:38:43 +090065 public static final String EXTRA_FAILURE = "com.android.documentsui.FAILURE";
Ben Kwacb4461f2015-05-05 11:50:11 -070066 public static final String EXTRA_TRANSFER_MODE = "com.android.documentsui.TRANSFER_MODE";
67
68 public static final int TRANSFER_MODE_NONE = 0;
69 public static final int TRANSFER_MODE_COPY = 1;
70 public static final int TRANSFER_MODE_MOVE = 2;
Tomasz Mikolajewski332d8192015-04-13 19:38:43 +090071
72 // TODO: Move it to a shared file when more operations are implemented.
73 public static final int FAILURE_COPY = 1;
Ben Kwa41b26c12015-03-31 10:11:43 -070074
75 private NotificationManager mNotificationManager;
76 private Notification.Builder mProgressBuilder;
77
78 // Jobs are serialized but a job ID is used, to avoid mixing up cancellation requests.
79 private String mJobId;
80 private volatile boolean mIsCancelled;
81 // Parameters of the copy job. Requests to an IntentService are serialized so this code only
82 // needs to deal with one job at a time.
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +090083 private final ArrayList<DocumentInfo> mFailedFiles;
Ben Kwa41b26c12015-03-31 10:11:43 -070084 private long mBatchSize;
85 private long mBytesCopied;
86 private long mStartTime;
87 private long mLastNotificationTime;
88 // Speed estimation
89 private long mBytesCopiedSample;
90 private long mSampleTime;
91 private long mSpeed;
92 private long mRemainingTime;
Ben Kwaf527c632015-04-08 15:03:35 -070093 // Provider clients are acquired for the duration of each copy job. Note that there is an
94 // implicit assumption that all srcs come from the same authority.
95 private ContentProviderClient mSrcClient;
96 private ContentProviderClient mDstClient;
Ben Kwa41b26c12015-03-31 10:11:43 -070097
98 public CopyService() {
99 super("CopyService");
Ben Kwaf527c632015-04-08 15:03:35 -0700100
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +0900101 mFailedFiles = new ArrayList<DocumentInfo>();
102 }
103
104 /**
105 * Starts the service for a copy operation.
106 *
107 * @param context Context for the intent.
108 * @param srcDocs A list of src files to copy.
109 * @param dstStack The copy destination stack.
110 */
Ben Kwac4693342015-09-30 10:00:10 -0700111 public static void start(Activity activity, List<DocumentInfo> srcDocs, DocumentStack dstStack,
Ben Kwacb4461f2015-05-05 11:50:11 -0700112 int mode) {
Ben Kwac4693342015-09-30 10:00:10 -0700113 final Resources res = activity.getResources();
114 final Intent copyIntent = new Intent(activity, CopyService.class);
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +0900115 copyIntent.putParcelableArrayListExtra(
116 EXTRA_SRC_LIST, new ArrayList<DocumentInfo>(srcDocs));
Steve McKay4d0255f2015-09-25 16:02:56 -0700117 copyIntent.putExtra(Shared.EXTRA_STACK, (Parcelable) dstStack);
Ben Kwacb4461f2015-05-05 11:50:11 -0700118 copyIntent.putExtra(EXTRA_TRANSFER_MODE, mode);
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +0900119
Ben Kwacb4461f2015-05-05 11:50:11 -0700120 int toastMessage = (mode == TRANSFER_MODE_COPY) ? R.plurals.copy_begin
121 : R.plurals.move_begin;
Ben Kwac4693342015-09-30 10:00:10 -0700122 Shared.makeSnackbar(activity,
Ben Kwacb4461f2015-05-05 11:50:11 -0700123 res.getQuantityString(toastMessage, srcDocs.size(), srcDocs.size()),
Ben Kwac4693342015-09-30 10:00:10 -0700124 Snackbar.LENGTH_SHORT).show();
125 activity.startService(copyIntent);
Ben Kwa41b26c12015-03-31 10:11:43 -0700126 }
127
128 @Override
129 public int onStartCommand(Intent intent, int flags, int startId) {
130 if (intent.hasExtra(EXTRA_CANCEL)) {
131 handleCancel(intent);
132 }
133 return super.onStartCommand(intent, flags, startId);
134 }
135
136 @Override
137 protected void onHandleIntent(Intent intent) {
138 if (intent.hasExtra(EXTRA_CANCEL)) {
139 handleCancel(intent);
140 return;
141 }
142
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +0900143 final ArrayList<DocumentInfo> srcs = intent.getParcelableArrayListExtra(EXTRA_SRC_LIST);
Steve McKay4d0255f2015-09-25 16:02:56 -0700144 final DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);
Ben Kwacb4461f2015-05-05 11:50:11 -0700145 // Copy by default.
146 final int transferMode = intent.getIntExtra(EXTRA_TRANSFER_MODE, TRANSFER_MODE_COPY);
Ben Kwa41b26c12015-03-31 10:11:43 -0700147
Ben Kwaf527c632015-04-08 15:03:35 -0700148 try {
149 // Acquire content providers.
150 mSrcClient = DocumentsApplication.acquireUnstableProviderOrThrow(getContentResolver(),
151 srcs.get(0).authority);
152 mDstClient = DocumentsApplication.acquireUnstableProviderOrThrow(getContentResolver(),
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +0900153 stack.peek().authority);
Ben Kwa41b26c12015-03-31 10:11:43 -0700154
Ben Kwacf870112015-05-28 16:18:59 -0700155 setupCopyJob(srcs, stack, transferMode);
Ben Kwaf527c632015-04-08 15:03:35 -0700156
157 for (int i = 0; i < srcs.size() && !mIsCancelled; ++i) {
Ben Kwacb4461f2015-05-05 11:50:11 -0700158 copy(srcs.get(i), stack.peek(), transferMode);
Ben Kwa41b26c12015-03-31 10:11:43 -0700159 }
Ben Kwaf527c632015-04-08 15:03:35 -0700160 } catch (Exception e) {
161 // Catch-all to prevent any copy errors from wedging the app.
162 Log.e(TAG, "Exceptions occurred during copying", e);
163 } finally {
Ben Kwa5f881992015-08-31 15:57:19 -0700164 if (DEBUG) Log.d(TAG, "Cleaning up after copy");
Ben Kwaf527c632015-04-08 15:03:35 -0700165 ContentProviderClient.releaseQuietly(mSrcClient);
166 ContentProviderClient.releaseQuietly(mDstClient);
167
168 // Dismiss the ongoing copy notification when the copy is done.
169 mNotificationManager.cancel(mJobId, 0);
170
171 if (mFailedFiles.size() > 0) {
Ben Kwa5f881992015-08-31 15:57:19 -0700172 Log.e(TAG, mFailedFiles.size() + " files failed to copy");
Tomasz Mikolajewski332d8192015-04-13 19:38:43 +0900173 final Context context = getApplicationContext();
Steve McKay0fbfc652015-08-20 16:48:49 -0700174 final Intent navigateIntent = new Intent(context, FilesActivity.class);
Steve McKay4d0255f2015-09-25 16:02:56 -0700175 navigateIntent.putExtra(Shared.EXTRA_STACK, (Parcelable) stack);
Tomasz Mikolajewski332d8192015-04-13 19:38:43 +0900176 navigateIntent.putExtra(EXTRA_FAILURE, FAILURE_COPY);
Ben Kwa5f881992015-08-31 15:57:19 -0700177 navigateIntent.putExtra(EXTRA_TRANSFER_MODE, transferMode);
Tomasz Mikolajewski332d8192015-04-13 19:38:43 +0900178 navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, mFailedFiles);
179
Ben Kwacf870112015-05-28 16:18:59 -0700180 final int titleResourceId = (transferMode == TRANSFER_MODE_COPY ?
181 R.plurals.copy_error_notification_title :
182 R.plurals.move_error_notification_title);
Tomasz Mikolajewski332d8192015-04-13 19:38:43 +0900183 final Notification.Builder errorBuilder = new Notification.Builder(this)
Ben Kwacf870112015-05-28 16:18:59 -0700184 .setContentTitle(context.getResources().getQuantityString(titleResourceId,
185 mFailedFiles.size(), mFailedFiles.size()))
Tomasz Mikolajewski332d8192015-04-13 19:38:43 +0900186 .setContentText(getString(R.string.notification_touch_for_details))
187 .setContentIntent(PendingIntent.getActivity(context, 0, navigateIntent,
188 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
189 .setCategory(Notification.CATEGORY_ERROR)
190 .setSmallIcon(R.drawable.ic_menu_copy)
191 .setAutoCancel(true);
192 mNotificationManager.notify(mJobId, 0, errorBuilder.build());
Ben Kwaf527c632015-04-08 15:03:35 -0700193 }
Ben Kwa5f881992015-08-31 15:57:19 -0700194 if (DEBUG) Log.d(TAG, "Done cleaning up");
Ben Kwa41b26c12015-03-31 10:11:43 -0700195 }
Ben Kwa41b26c12015-03-31 10:11:43 -0700196 }
197
198 @Override
199 public void onCreate() {
200 super.onCreate();
201 mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
202 }
203
204 /**
205 * Sets up the CopyService to start tracking and sending notifications for the given batch of
206 * files.
207 *
208 * @param srcs A list of src files to copy.
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +0900209 * @param stack The copy destination stack.
Ben Kwacf870112015-05-28 16:18:59 -0700210 * @param transferMode The mode (i.e. copy, or move)
Ben Kwaf527c632015-04-08 15:03:35 -0700211 * @throws RemoteException
Ben Kwa41b26c12015-03-31 10:11:43 -0700212 */
Ben Kwacf870112015-05-28 16:18:59 -0700213 private void setupCopyJob(ArrayList<DocumentInfo> srcs, DocumentStack stack, int transferMode)
Ben Kwaf527c632015-04-08 15:03:35 -0700214 throws RemoteException {
Ben Kwacf870112015-05-28 16:18:59 -0700215 final boolean copying = (transferMode == TRANSFER_MODE_COPY);
Ben Kwa41b26c12015-03-31 10:11:43 -0700216 // Create an ID for this copy job. Use the timestamp.
217 mJobId = String.valueOf(SystemClock.elapsedRealtime());
218 // Reset the cancellation flag.
219 mIsCancelled = false;
220
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +0900221 final Context context = getApplicationContext();
Steve McKay0fbfc652015-08-20 16:48:49 -0700222 final Intent navigateIntent = new Intent(context, FilesActivity.class);
Steve McKay4d0255f2015-09-25 16:02:56 -0700223 navigateIntent.putExtra(Shared.EXTRA_STACK, (Parcelable) stack);
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +0900224
Ben Kwacf870112015-05-28 16:18:59 -0700225 final String contentTitle = getString(copying ? R.string.copy_notification_title
226 : R.string.move_notification_title);
Ben Kwa41b26c12015-03-31 10:11:43 -0700227 mProgressBuilder = new Notification.Builder(this)
Ben Kwacf870112015-05-28 16:18:59 -0700228 .setContentTitle(contentTitle)
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +0900229 .setContentIntent(PendingIntent.getActivity(context, 0, navigateIntent, 0))
Ben Kwa41b26c12015-03-31 10:11:43 -0700230 .setCategory(Notification.CATEGORY_PROGRESS)
Tomasz Mikolajewski332d8192015-04-13 19:38:43 +0900231 .setSmallIcon(R.drawable.ic_menu_copy)
232 .setOngoing(true);
Ben Kwa41b26c12015-03-31 10:11:43 -0700233
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +0900234 final Intent cancelIntent = new Intent(this, CopyService.class);
Ben Kwa41b26c12015-03-31 10:11:43 -0700235 cancelIntent.putExtra(EXTRA_CANCEL, mJobId);
236 mProgressBuilder.addAction(R.drawable.ic_cab_cancel,
Daichi Hirono677ce6c2015-04-15 14:33:52 +0900237 getString(android.R.string.cancel), PendingIntent.getService(this, 0,
Ben Kwac89eb7c2015-04-15 12:13:32 -0700238 cancelIntent,
239 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT));
Ben Kwa41b26c12015-03-31 10:11:43 -0700240
Ben Kwa41b26c12015-03-31 10:11:43 -0700241 // Send an initial progress notification.
Ben Kwacf870112015-05-28 16:18:59 -0700242 final String contentText = getString(copying ? R.string.copy_preparing
243 : R.string.move_preparing);
Ben Kwaf527c632015-04-08 15:03:35 -0700244 mProgressBuilder.setProgress(0, 0, true); // Indeterminate progress while setting up.
Ben Kwacf870112015-05-28 16:18:59 -0700245 mProgressBuilder.setContentText(contentText);
Ben Kwa41b26c12015-03-31 10:11:43 -0700246 mNotificationManager.notify(mJobId, 0, mProgressBuilder.build());
247
248 // Reset batch parameters.
Ben Kwaf527c632015-04-08 15:03:35 -0700249 mFailedFiles.clear();
250 mBatchSize = calculateFileSizes(srcs);
Ben Kwa41b26c12015-03-31 10:11:43 -0700251 mBytesCopied = 0;
252 mStartTime = SystemClock.elapsedRealtime();
253 mLastNotificationTime = 0;
254 mBytesCopiedSample = 0;
255 mSampleTime = 0;
256 mSpeed = 0;
257 mRemainingTime = 0;
258
259 // TODO: Check preconditions for copy.
260 // - check that the destination has enough space and is writeable?
261 // - check MIME types?
262 }
263
264 /**
Ben Kwaf527c632015-04-08 15:03:35 -0700265 * Calculates the cumulative size of all the documents in the list. Directories are recursed
266 * into and totaled up.
267 *
268 * @param srcs
269 * @return Size in bytes.
270 * @throws RemoteException
271 */
272 private long calculateFileSizes(List<DocumentInfo> srcs) throws RemoteException {
273 long result = 0;
274 for (DocumentInfo src : srcs) {
275 if (Document.MIME_TYPE_DIR.equals(src.mimeType)) {
276 // Directories need to be recursed into.
277 result += calculateFileSizesHelper(src.derivedUri);
278 } else {
279 result += src.size;
280 }
281 }
282 return result;
283 }
284
285 /**
286 * Calculates (recursively) the cumulative size of all the files under the given directory.
287 *
288 * @throws RemoteException
289 */
290 private long calculateFileSizesHelper(Uri uri) throws RemoteException {
291 final String authority = uri.getAuthority();
292 final Uri queryUri = DocumentsContract.buildChildDocumentsUri(authority,
293 DocumentsContract.getDocumentId(uri));
294 final String queryColumns[] = new String[] {
295 Document.COLUMN_DOCUMENT_ID,
296 Document.COLUMN_MIME_TYPE,
297 Document.COLUMN_SIZE
298 };
299
300 long result = 0;
301 Cursor cursor = null;
302 try {
303 cursor = mSrcClient.query(queryUri, queryColumns, null, null, null);
304 while (cursor.moveToNext()) {
305 if (Document.MIME_TYPE_DIR.equals(
306 getCursorString(cursor, Document.COLUMN_MIME_TYPE))) {
307 // Recurse into directories.
308 final Uri subdirUri = DocumentsContract.buildDocumentUri(authority,
309 getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
310 result += calculateFileSizesHelper(subdirUri);
311 } else {
312 // This may return -1 if the size isn't defined. Ignore those cases.
313 long size = getCursorLong(cursor, Document.COLUMN_SIZE);
314 result += size > 0 ? size : 0;
315 }
316 }
317 } finally {
318 IoUtils.closeQuietly(cursor);
319 }
320
321 return result;
322 }
323
324 /**
Ben Kwa41b26c12015-03-31 10:11:43 -0700325 * Cancels the current copy job, if its ID matches the given ID.
326 *
327 * @param intent The cancellation intent.
328 */
329 private void handleCancel(Intent intent) {
330 final String cancelledId = intent.getStringExtra(EXTRA_CANCEL);
331 // Do nothing if the cancelled ID doesn't match the current job ID. This prevents racey
Ben Kwa6cbab7d2015-05-27 09:18:53 -0700332 // cancellation requests from affecting unrelated copy jobs. However, if the current job ID
333 // is null, the service most likely crashed and was revived by the incoming cancel intent.
334 // In that case, always allow the cancellation to proceed.
335 if (Objects.equals(mJobId, cancelledId) || mJobId == null) {
Ben Kwa41b26c12015-03-31 10:11:43 -0700336 // Set the cancel flag. This causes the copy loops to exit.
337 mIsCancelled = true;
338 // Dismiss the progress notification here rather than in the copy loop. This preserves
339 // interactivity for the user in case the copy loop is stalled.
Ben Kwa6cbab7d2015-05-27 09:18:53 -0700340 mNotificationManager.cancel(cancelledId, 0);
Ben Kwa41b26c12015-03-31 10:11:43 -0700341 }
342 }
343
344 /**
345 * Logs progress on the current copy operation. Displays/Updates the progress notification.
346 *
347 * @param bytesCopied
348 */
349 private void makeProgress(long bytesCopied) {
350 mBytesCopied += bytesCopied;
351 double done = (double) mBytesCopied / mBatchSize;
352 String percent = NumberFormat.getPercentInstance().format(done);
353
354 // Update time estimate
355 long currentTime = SystemClock.elapsedRealtime();
356 long elapsedTime = currentTime - mStartTime;
357
358 // Send out progress notifications once a second.
359 if (currentTime - mLastNotificationTime > 1000) {
360 updateRemainingTimeEstimate(elapsedTime);
361 mProgressBuilder.setProgress(100, (int) (done * 100), false);
362 mProgressBuilder.setContentInfo(percent);
363 if (mRemainingTime > 0) {
364 mProgressBuilder.setContentText(getString(R.string.copy_remaining,
365 DateUtils.formatDuration(mRemainingTime)));
366 } else {
367 mProgressBuilder.setContentText(null);
368 }
369 mNotificationManager.notify(mJobId, 0, mProgressBuilder.build());
370 mLastNotificationTime = currentTime;
371 }
372 }
373
374 /**
375 * Generates an estimate of the remaining time in the copy.
376 *
377 * @param elapsedTime The time elapsed so far.
378 */
379 private void updateRemainingTimeEstimate(long elapsedTime) {
380 final long sampleDuration = elapsedTime - mSampleTime;
381 final long sampleSpeed = ((mBytesCopied - mBytesCopiedSample) * 1000) / sampleDuration;
382 if (mSpeed == 0) {
383 mSpeed = sampleSpeed;
384 } else {
385 mSpeed = ((3 * mSpeed) + sampleSpeed) / 4;
386 }
387
388 if (mSampleTime > 0 && mSpeed > 0) {
389 mRemainingTime = ((mBatchSize - mBytesCopied) * 1000) / mSpeed;
390 } else {
391 mRemainingTime = 0;
392 }
393
394 mSampleTime = elapsedTime;
395 mBytesCopiedSample = mBytesCopied;
396 }
397
398 /**
Ben Kwaf527c632015-04-08 15:03:35 -0700399 * Copies a the given documents to the given location.
Ben Kwa41b26c12015-03-31 10:11:43 -0700400 *
Ben Kwaf527c632015-04-08 15:03:35 -0700401 * @param srcInfo DocumentInfos for the documents to copy.
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +0900402 * @param dstDirInfo The destination directory.
Ben Kwaf527c632015-04-08 15:03:35 -0700403 * @throws RemoteException
Ben Kwa41b26c12015-03-31 10:11:43 -0700404 */
Ben Kwacb4461f2015-05-05 11:50:11 -0700405 private void copy(DocumentInfo srcInfo, DocumentInfo dstDirInfo, int mode)
406 throws RemoteException {
Ben Kwa5f881992015-08-31 15:57:19 -0700407 if (DEBUG) Log.d(TAG, "Copying " + srcInfo.displayName + " (" + srcInfo.derivedUri + ")" +
408 " to " + dstDirInfo.displayName + " (" + dstDirInfo.derivedUri + ")");
409
Tomasz Mikolajewski2023fcf2015-04-10 10:30:33 +0900410 final Uri dstUri = DocumentsContract.createDocument(mDstClient, dstDirInfo.derivedUri,
Ben Kwaf5858932015-04-07 15:43:39 -0700411 srcInfo.mimeType, srcInfo.displayName);
Ben Kwaf527c632015-04-08 15:03:35 -0700412 if (dstUri == null) {
413 // If this is a directory, the entire subdir will not be copied over.
414 Log.e(TAG, "Error while copying " + srcInfo.displayName);
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +0900415 mFailedFiles.add(srcInfo);
Ben Kwaf527c632015-04-08 15:03:35 -0700416 return;
417 }
Ben Kwaf5858932015-04-07 15:43:39 -0700418
Ben Kwaf527c632015-04-08 15:03:35 -0700419 if (Document.MIME_TYPE_DIR.equals(srcInfo.mimeType)) {
Ben Kwacb4461f2015-05-05 11:50:11 -0700420 copyDirectoryHelper(srcInfo.derivedUri, dstUri, mode);
Ben Kwaf527c632015-04-08 15:03:35 -0700421 } else {
Ben Kwacb4461f2015-05-05 11:50:11 -0700422 copyFileHelper(srcInfo.derivedUri, dstUri, mode);
Ben Kwaf527c632015-04-08 15:03:35 -0700423 }
424 }
425
426 /**
427 * Handles recursion into a directory and copying its contents. Note that in linux terms, this
428 * does the equivalent of "cp src/* dst", not "cp -r src dst".
429 *
430 * @param srcDirUri URI of the directory to copy from. The routine will copy the directory's
431 * contents, not the directory itself.
432 * @param dstDirUri URI of the directory to copy to. Must be created beforehand.
433 * @throws RemoteException
434 */
Ben Kwacb4461f2015-05-05 11:50:11 -0700435 private void copyDirectoryHelper(Uri srcDirUri, Uri dstDirUri, int mode)
436 throws RemoteException {
Ben Kwaf527c632015-04-08 15:03:35 -0700437 // Recurse into directories. Copy children into the new subdirectory.
438 final String queryColumns[] = new String[] {
439 Document.COLUMN_DISPLAY_NAME,
440 Document.COLUMN_DOCUMENT_ID,
441 Document.COLUMN_MIME_TYPE,
442 Document.COLUMN_SIZE
443 };
444 final Uri queryUri = DocumentsContract.buildChildDocumentsUri(srcDirUri.getAuthority(),
445 DocumentsContract.getDocumentId(srcDirUri));
446 Cursor cursor = null;
447 try {
448 // Iterate over srcs in the directory; copy to the destination directory.
449 cursor = mSrcClient.query(queryUri, queryColumns, null, null, null);
450 while (cursor.moveToNext()) {
451 final String childMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
452 final Uri dstUri = DocumentsContract.createDocument(mDstClient, dstDirUri,
453 childMimeType, getCursorString(cursor, Document.COLUMN_DISPLAY_NAME));
454 final Uri childUri = DocumentsContract.buildDocumentUri(srcDirUri.getAuthority(),
455 getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
456 if (Document.MIME_TYPE_DIR.equals(childMimeType)) {
Ben Kwacb4461f2015-05-05 11:50:11 -0700457 copyDirectoryHelper(childUri, dstUri, mode);
Ben Kwaf527c632015-04-08 15:03:35 -0700458 } else {
Ben Kwacb4461f2015-05-05 11:50:11 -0700459 copyFileHelper(childUri, dstUri, mode);
460 }
461 }
462 if (mode == TRANSFER_MODE_MOVE) {
463 try {
464 DocumentsContract.deleteDocument(mSrcClient, srcDirUri);
465 } catch (RemoteException e) {
466 // RemoteExceptions usually signal that the connection is dead, so there's no
467 // point attempting to continue. Propagate the exception up so the copy job is
468 // cancelled.
469 Log.w(TAG, "Failed to clean up after move: " + srcDirUri, e);
470 throw e;
Ben Kwaf527c632015-04-08 15:03:35 -0700471 }
472 }
473 } finally {
474 IoUtils.closeQuietly(cursor);
475 }
476 }
477
478 /**
479 * Handles copying a single file.
480 *
481 * @param srcUri URI of the file to copy from.
482 * @param dstUri URI of the *file* to copy to. Must be created beforehand.
483 * @throws RemoteException
484 */
Ben Kwacb4461f2015-05-05 11:50:11 -0700485 private void copyFileHelper(Uri srcUri, Uri dstUri, int mode)
486 throws RemoteException {
Ben Kwaf527c632015-04-08 15:03:35 -0700487 // Copy an individual file.
Ben Kwaf5858932015-04-07 15:43:39 -0700488 CancellationSignal canceller = new CancellationSignal();
489 ParcelFileDescriptor srcFile = null;
490 ParcelFileDescriptor dstFile = null;
491 InputStream src = null;
492 OutputStream dst = null;
Ben Kwa41b26c12015-03-31 10:11:43 -0700493
Ben Kwa37c76592015-04-24 15:35:25 -0700494 IOException copyError = null;
Ben Kwa41b26c12015-03-31 10:11:43 -0700495 try {
Ben Kwaf527c632015-04-08 15:03:35 -0700496 srcFile = mSrcClient.openFile(srcUri, "r", canceller);
497 dstFile = mDstClient.openFile(dstUri, "w", canceller);
Ben Kwaf5858932015-04-07 15:43:39 -0700498 src = new ParcelFileDescriptor.AutoCloseInputStream(srcFile);
499 dst = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);
Ben Kwa41b26c12015-03-31 10:11:43 -0700500
501 byte[] buffer = new byte[8192];
502 int len;
Ben Kwaf5858932015-04-07 15:43:39 -0700503 while (!mIsCancelled && ((len = src.read(buffer)) != -1)) {
504 dst.write(buffer, 0, len);
Ben Kwa41b26c12015-03-31 10:11:43 -0700505 makeProgress(len);
506 }
Ben Kwa37c76592015-04-24 15:35:25 -0700507
Ben Kwaf5858932015-04-07 15:43:39 -0700508 srcFile.checkError();
Ben Kwa41b26c12015-03-31 10:11:43 -0700509 } catch (IOException e) {
Ben Kwa37c76592015-04-24 15:35:25 -0700510 copyError = e;
Ben Kwa5f881992015-08-31 15:57:19 -0700511
Ben Kwa2d88d492015-05-05 11:58:38 -0700512 try {
Ben Kwa5f881992015-08-31 15:57:19 -0700513 DocumentInfo info = DocumentInfo.fromUri(getContentResolver(), srcUri);
514 mFailedFiles.add(info);
515 Log.e(TAG, "Error while copying " + info.displayName + " (" + info.derivedUri + ")",
516 copyError);
517 } catch (FileNotFoundException ignore) {
518 // Generate a dummy DocumentInfo so an error still gets reflected in the UI for this
519 // file.
520 DocumentInfo info = new DocumentInfo();
521 info.derivedUri = srcUri;
522 info.displayName = "Unknown [" + srcUri + "]";
523 mFailedFiles.add(info);
524 Log.e(TAG, "Error while copying " + srcUri, copyError);
525 }
526
527 if (dstFile != null) {
528 try {
529 dstFile.closeWithError(copyError.getMessage());
530 } catch (IOException closeError) {
531 Log.e(TAG, "Error closing destination", closeError);
532 }
Ben Kwa37c76592015-04-24 15:35:25 -0700533 }
Ben Kwa2d88d492015-05-05 11:58:38 -0700534 } finally {
Ben Kwaf5858932015-04-07 15:43:39 -0700535 // This also ensures the file descriptors are closed.
536 IoUtils.closeQuietly(src);
537 IoUtils.closeQuietly(dst);
Ben Kwa41b26c12015-03-31 10:11:43 -0700538 }
539
Ben Kwa37c76592015-04-24 15:35:25 -0700540
541 if (copyError != null || mIsCancelled) {
Ben Kwa41b26c12015-03-31 10:11:43 -0700542 // Clean up half-copied files.
Ben Kwaf5858932015-04-07 15:43:39 -0700543 canceller.cancel();
Ben Kwaf527c632015-04-08 15:03:35 -0700544 try {
545 DocumentsContract.deleteDocument(mDstClient, dstUri);
546 } catch (RemoteException e) {
Ben Kwacb4461f2015-05-05 11:50:11 -0700547 Log.w(TAG, "Failed to clean up after copy error: " + dstUri, e);
Ben Kwaf527c632015-04-08 15:03:35 -0700548 // RemoteExceptions usually signal that the connection is dead, so there's no point
549 // attempting to continue. Propagate the exception up so the copy job is cancelled.
550 throw e;
Ben Kwa41b26c12015-03-31 10:11:43 -0700551 }
Ben Kwacb4461f2015-05-05 11:50:11 -0700552 } else if (mode == TRANSFER_MODE_MOVE) {
553 // Clean up src files after a successful move.
554 try {
555 DocumentsContract.deleteDocument(mSrcClient, srcUri);
556 } catch (RemoteException e) {
557 Log.w(TAG, "Failed to clean up after move: " + srcUri, e);
558 throw e;
559 }
Ben Kwa41b26c12015-03-31 10:11:43 -0700560 }
561 }
562}