1d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa/* 2d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * Copyright (C) 2015 The Android Open Source Project 3d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * 4d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * Licensed under the Apache License, Version 2.0 (the "License"); 5d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * you may not use this file except in compliance with the License. 6d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * You may obtain a copy of the License at 7d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * 8d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * http://www.apache.org/licenses/LICENSE-2.0 9d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * 10d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * Unless required by applicable law or agreed to in writing, software 11d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * distributed under the License is distributed on an "AS IS" BASIS, 12d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * See the License for the specific language governing permissions and 14d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * limitations under the License. 15d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa */ 16d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 17d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwapackage com.android.documentsui; 18d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 19726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwaimport static com.android.documentsui.model.DocumentInfo.getCursorLong; 20726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwaimport static com.android.documentsui.model.DocumentInfo.getCursorString; 21726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa 22d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwaimport android.app.IntentService; 23d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwaimport android.app.Notification; 24d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwaimport android.app.NotificationManager; 25d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwaimport android.app.PendingIntent; 26726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwaimport android.content.ContentProviderClient; 27d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwaimport android.content.Context; 28d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwaimport android.content.Intent; 29f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewskiimport android.content.res.Resources; 30726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwaimport android.database.Cursor; 31d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwaimport android.net.Uri; 32ef3f2620b3a755856d70345fc7a90df896985c26Ben Kwaimport android.os.CancellationSignal; 33ef3f2620b3a755856d70345fc7a90df896985c26Ben Kwaimport android.os.ParcelFileDescriptor; 34e1a03f8eec2719279037ab348df306764dc45a70Tomasz Mikolajewskiimport android.os.Parcelable; 35726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwaimport android.os.RemoteException; 36d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwaimport android.os.SystemClock; 37ef3f2620b3a755856d70345fc7a90df896985c26Ben Kwaimport android.provider.DocumentsContract; 38726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwaimport android.provider.DocumentsContract.Document; 39d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwaimport android.text.format.DateUtils; 40d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwaimport android.util.Log; 41f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewskiimport android.widget.Toast; 42d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 43d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwaimport com.android.documentsui.model.DocumentInfo; 44e1a03f8eec2719279037ab348df306764dc45a70Tomasz Mikolajewskiimport com.android.documentsui.model.DocumentStack; 45d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 46d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwaimport libcore.io.IoUtils; 47d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 48f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewskiimport java.io.FileNotFoundException; 49d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwaimport java.io.IOException; 50d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwaimport java.io.InputStream; 51d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwaimport java.io.OutputStream; 52d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwaimport java.text.NumberFormat; 53d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwaimport java.util.ArrayList; 54726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwaimport java.util.List; 55726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwaimport java.util.Objects; 56d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 57d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwapublic class CopyService extends IntentService { 58d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa public static final String TAG = "CopyService"; 59e1a03f8eec2719279037ab348df306764dc45a70Tomasz Mikolajewski 60d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa private static final String EXTRA_CANCEL = "com.android.documentsui.CANCEL"; 61e1a03f8eec2719279037ab348df306764dc45a70Tomasz Mikolajewski public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST"; 62e1a03f8eec2719279037ab348df306764dc45a70Tomasz Mikolajewski public static final String EXTRA_STACK = "com.android.documentsui.STACK"; 6361686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski public static final String EXTRA_FAILURE = "com.android.documentsui.FAILURE"; 6461686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski 6561686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski // TODO: Move it to a shared file when more operations are implemented. 6661686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski public static final int FAILURE_COPY = 1; 67d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 68d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa private NotificationManager mNotificationManager; 69d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa private Notification.Builder mProgressBuilder; 70d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 71d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa // Jobs are serialized but a job ID is used, to avoid mixing up cancellation requests. 72d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa private String mJobId; 73d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa private volatile boolean mIsCancelled; 74d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa // Parameters of the copy job. Requests to an IntentService are serialized so this code only 75d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa // needs to deal with one job at a time. 76f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski private final ArrayList<DocumentInfo> mFailedFiles; 77d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa private long mBatchSize; 78d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa private long mBytesCopied; 79d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa private long mStartTime; 80d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa private long mLastNotificationTime; 81d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa // Speed estimation 82d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa private long mBytesCopiedSample; 83d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa private long mSampleTime; 84d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa private long mSpeed; 85d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa private long mRemainingTime; 86726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa // Provider clients are acquired for the duration of each copy job. Note that there is an 87726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa // implicit assumption that all srcs come from the same authority. 88726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa private ContentProviderClient mSrcClient; 89726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa private ContentProviderClient mDstClient; 90d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 91d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa public CopyService() { 92d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa super("CopyService"); 93726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa 94f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski mFailedFiles = new ArrayList<DocumentInfo>(); 95f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski } 96f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski 97f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski /** 98f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski * Starts the service for a copy operation. 99f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski * 100f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski * @param context Context for the intent. 101f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski * @param srcDocs A list of src files to copy. 102f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski * @param dstStack The copy destination stack. 103f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski */ 104f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski public static void start(Context context, List<DocumentInfo> srcDocs, DocumentStack dstStack) { 105f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski final Resources res = context.getResources(); 106f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski final Intent copyIntent = new Intent(context, CopyService.class); 107f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski copyIntent.putParcelableArrayListExtra( 108f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski EXTRA_SRC_LIST, new ArrayList<DocumentInfo>(srcDocs)); 109f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski copyIntent.putExtra(EXTRA_STACK, (Parcelable) dstStack); 110f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski 111f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski Toast.makeText(context, 112f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski res.getQuantityString(R.plurals.copy_begin, srcDocs.size(), srcDocs.size()), 113f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski Toast.LENGTH_SHORT).show(); 114f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski context.startService(copyIntent); 115d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } 116d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 117d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa @Override 118d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa public int onStartCommand(Intent intent, int flags, int startId) { 119d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa if (intent.hasExtra(EXTRA_CANCEL)) { 120d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa handleCancel(intent); 121d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } 122d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa return super.onStartCommand(intent, flags, startId); 123d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } 124d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 125d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa @Override 126d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa protected void onHandleIntent(Intent intent) { 127d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa if (intent.hasExtra(EXTRA_CANCEL)) { 128d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa handleCancel(intent); 129d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa return; 130d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } 131d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 132e1a03f8eec2719279037ab348df306764dc45a70Tomasz Mikolajewski final ArrayList<DocumentInfo> srcs = intent.getParcelableArrayListExtra(EXTRA_SRC_LIST); 133e1a03f8eec2719279037ab348df306764dc45a70Tomasz Mikolajewski final DocumentStack stack = intent.getParcelableExtra(EXTRA_STACK); 134d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 135726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa try { 136726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa // Acquire content providers. 137726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa mSrcClient = DocumentsApplication.acquireUnstableProviderOrThrow(getContentResolver(), 138726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa srcs.get(0).authority); 139726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa mDstClient = DocumentsApplication.acquireUnstableProviderOrThrow(getContentResolver(), 140e1a03f8eec2719279037ab348df306764dc45a70Tomasz Mikolajewski stack.peek().authority); 141d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 142e1a03f8eec2719279037ab348df306764dc45a70Tomasz Mikolajewski setupCopyJob(srcs, stack); 143726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa 144726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa for (int i = 0; i < srcs.size() && !mIsCancelled; ++i) { 145e1a03f8eec2719279037ab348df306764dc45a70Tomasz Mikolajewski copy(srcs.get(i), stack.peek()); 146d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } 147726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } catch (Exception e) { 148726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa // Catch-all to prevent any copy errors from wedging the app. 149726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa Log.e(TAG, "Exceptions occurred during copying", e); 150726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } finally { 151726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa ContentProviderClient.releaseQuietly(mSrcClient); 152726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa ContentProviderClient.releaseQuietly(mDstClient); 153d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 154726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa // Dismiss the ongoing copy notification when the copy is done. 155726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa mNotificationManager.cancel(mJobId, 0); 156d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 157726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa if (mFailedFiles.size() > 0) { 15861686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski final Context context = getApplicationContext(); 1596e02dc8cd69720043bd36ae29fe93e767e3b38a9Steve McKay final Intent navigateIntent = new Intent(context, DocumentsActivity.class); 16061686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski navigateIntent.putExtra(EXTRA_STACK, (Parcelable) stack); 16161686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski navigateIntent.putExtra(EXTRA_FAILURE, FAILURE_COPY); 16261686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, mFailedFiles); 16361686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski 16461686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski final Notification.Builder errorBuilder = new Notification.Builder(this) 16561686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski .setContentTitle(context.getResources(). 16661686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski getQuantityString(R.plurals.copy_error_notification_title, 16761686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski mFailedFiles.size(), mFailedFiles.size())) 16861686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski .setContentText(getString(R.string.notification_touch_for_details)) 16961686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski .setContentIntent(PendingIntent.getActivity(context, 0, navigateIntent, 17061686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT)) 17161686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski .setCategory(Notification.CATEGORY_ERROR) 17261686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski .setSmallIcon(R.drawable.ic_menu_copy) 17361686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski .setAutoCancel(true); 17461686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski mNotificationManager.notify(mJobId, 0, errorBuilder.build()); 175726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } 176726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } 177d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } 178d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 179d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa @Override 180d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa public void onCreate() { 181d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa super.onCreate(); 182d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 183d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } 184d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 185d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa /** 186d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * Sets up the CopyService to start tracking and sending notifications for the given batch of 187d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * files. 188d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * 189d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * @param srcs A list of src files to copy. 190e1a03f8eec2719279037ab348df306764dc45a70Tomasz Mikolajewski * @param stack The copy destination stack. 191726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * @throws RemoteException 192d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa */ 193e1a03f8eec2719279037ab348df306764dc45a70Tomasz Mikolajewski private void setupCopyJob(ArrayList<DocumentInfo> srcs, DocumentStack stack) 194726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa throws RemoteException { 195d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa // Create an ID for this copy job. Use the timestamp. 196d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mJobId = String.valueOf(SystemClock.elapsedRealtime()); 197d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa // Reset the cancellation flag. 198d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mIsCancelled = false; 199d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 200e1a03f8eec2719279037ab348df306764dc45a70Tomasz Mikolajewski final Context context = getApplicationContext(); 2016e02dc8cd69720043bd36ae29fe93e767e3b38a9Steve McKay final Intent navigateIntent = new Intent(context, DocumentsActivity.class); 20261686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski navigateIntent.putExtra(EXTRA_STACK, (Parcelable) stack); 203e1a03f8eec2719279037ab348df306764dc45a70Tomasz Mikolajewski 204d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mProgressBuilder = new Notification.Builder(this) 205d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa .setContentTitle(getString(R.string.copy_notification_title)) 206e1a03f8eec2719279037ab348df306764dc45a70Tomasz Mikolajewski .setContentIntent(PendingIntent.getActivity(context, 0, navigateIntent, 0)) 207d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa .setCategory(Notification.CATEGORY_PROGRESS) 20861686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski .setSmallIcon(R.drawable.ic_menu_copy) 20961686593fd32babb6e86754ea5bfa6bc95cd3690Tomasz Mikolajewski .setOngoing(true); 210d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 211e1a03f8eec2719279037ab348df306764dc45a70Tomasz Mikolajewski final Intent cancelIntent = new Intent(this, CopyService.class); 212d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa cancelIntent.putExtra(EXTRA_CANCEL, mJobId); 213d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mProgressBuilder.addAction(R.drawable.ic_cab_cancel, 214e4db58edd4a0f02b3ad0051fb7cc50829771528eDaichi Hirono getString(android.R.string.cancel), PendingIntent.getService(this, 0, 21594ac0d7d3069adc5074945cec3d7d5f5a64b4e7aBen Kwa cancelIntent, 21694ac0d7d3069adc5074945cec3d7d5f5a64b4e7aBen Kwa PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT)); 217d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 218d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa // Send an initial progress notification. 219726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa mProgressBuilder.setProgress(0, 0, true); // Indeterminate progress while setting up. 220726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa mProgressBuilder.setContentText(getString(R.string.copy_preparing)); 221d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mNotificationManager.notify(mJobId, 0, mProgressBuilder.build()); 222d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 223d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa // Reset batch parameters. 224726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa mFailedFiles.clear(); 225726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa mBatchSize = calculateFileSizes(srcs); 226d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mBytesCopied = 0; 227d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mStartTime = SystemClock.elapsedRealtime(); 228d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mLastNotificationTime = 0; 229d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mBytesCopiedSample = 0; 230d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mSampleTime = 0; 231d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mSpeed = 0; 232d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mRemainingTime = 0; 233d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 234d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa // TODO: Check preconditions for copy. 235d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa // - check that the destination has enough space and is writeable? 236d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa // - check MIME types? 237d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } 238d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 239d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa /** 240726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * Calculates the cumulative size of all the documents in the list. Directories are recursed 241726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * into and totaled up. 242726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * 243726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * @param srcs 244726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * @return Size in bytes. 245726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * @throws RemoteException 246726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa */ 247726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa private long calculateFileSizes(List<DocumentInfo> srcs) throws RemoteException { 248726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa long result = 0; 249726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa for (DocumentInfo src : srcs) { 250726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa if (Document.MIME_TYPE_DIR.equals(src.mimeType)) { 251726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa // Directories need to be recursed into. 252726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa result += calculateFileSizesHelper(src.derivedUri); 253726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } else { 254726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa result += src.size; 255726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } 256726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } 257726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa return result; 258726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } 259726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa 260726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa /** 261726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * Calculates (recursively) the cumulative size of all the files under the given directory. 262726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * 263726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * @throws RemoteException 264726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa */ 265726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa private long calculateFileSizesHelper(Uri uri) throws RemoteException { 266726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa final String authority = uri.getAuthority(); 267726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa final Uri queryUri = DocumentsContract.buildChildDocumentsUri(authority, 268726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa DocumentsContract.getDocumentId(uri)); 269726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa final String queryColumns[] = new String[] { 270726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa Document.COLUMN_DOCUMENT_ID, 271726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa Document.COLUMN_MIME_TYPE, 272726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa Document.COLUMN_SIZE 273726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa }; 274726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa 275726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa long result = 0; 276726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa Cursor cursor = null; 277726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa try { 278726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa cursor = mSrcClient.query(queryUri, queryColumns, null, null, null); 279726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa while (cursor.moveToNext()) { 280726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa if (Document.MIME_TYPE_DIR.equals( 281726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa getCursorString(cursor, Document.COLUMN_MIME_TYPE))) { 282726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa // Recurse into directories. 283726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa final Uri subdirUri = DocumentsContract.buildDocumentUri(authority, 284726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa getCursorString(cursor, Document.COLUMN_DOCUMENT_ID)); 285726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa result += calculateFileSizesHelper(subdirUri); 286726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } else { 287726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa // This may return -1 if the size isn't defined. Ignore those cases. 288726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa long size = getCursorLong(cursor, Document.COLUMN_SIZE); 289726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa result += size > 0 ? size : 0; 290726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } 291726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } 292726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } finally { 293726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa IoUtils.closeQuietly(cursor); 294726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } 295726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa 296726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa return result; 297726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } 298726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa 299726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa /** 300d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * Cancels the current copy job, if its ID matches the given ID. 301d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * 302d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * @param intent The cancellation intent. 303d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa */ 304d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa private void handleCancel(Intent intent) { 305d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa final String cancelledId = intent.getStringExtra(EXTRA_CANCEL); 306d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa // Do nothing if the cancelled ID doesn't match the current job ID. This prevents racey 3077c0ade56f38c61055e7d35a5e077d4f6f005b5bdBen Kwa // cancellation requests from affecting unrelated copy jobs. However, if the current job ID 3087c0ade56f38c61055e7d35a5e077d4f6f005b5bdBen Kwa // is null, the service most likely crashed and was revived by the incoming cancel intent. 3097c0ade56f38c61055e7d35a5e077d4f6f005b5bdBen Kwa // In that case, always allow the cancellation to proceed. 3107c0ade56f38c61055e7d35a5e077d4f6f005b5bdBen Kwa if (Objects.equals(mJobId, cancelledId) || mJobId == null) { 311d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa // Set the cancel flag. This causes the copy loops to exit. 312d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mIsCancelled = true; 313d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa // Dismiss the progress notification here rather than in the copy loop. This preserves 314d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa // interactivity for the user in case the copy loop is stalled. 3157c0ade56f38c61055e7d35a5e077d4f6f005b5bdBen Kwa mNotificationManager.cancel(cancelledId, 0); 316d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } 317d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } 318d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 319d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa /** 320d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * Logs progress on the current copy operation. Displays/Updates the progress notification. 321d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * 322d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * @param bytesCopied 323d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa */ 324d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa private void makeProgress(long bytesCopied) { 325d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mBytesCopied += bytesCopied; 326d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa double done = (double) mBytesCopied / mBatchSize; 327d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa String percent = NumberFormat.getPercentInstance().format(done); 328d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 329d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa // Update time estimate 330d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa long currentTime = SystemClock.elapsedRealtime(); 331d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa long elapsedTime = currentTime - mStartTime; 332d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 333d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa // Send out progress notifications once a second. 334d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa if (currentTime - mLastNotificationTime > 1000) { 335d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa updateRemainingTimeEstimate(elapsedTime); 336d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mProgressBuilder.setProgress(100, (int) (done * 100), false); 337d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mProgressBuilder.setContentInfo(percent); 338d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa if (mRemainingTime > 0) { 339d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mProgressBuilder.setContentText(getString(R.string.copy_remaining, 340d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa DateUtils.formatDuration(mRemainingTime))); 341d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } else { 342d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mProgressBuilder.setContentText(null); 343d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } 344d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mNotificationManager.notify(mJobId, 0, mProgressBuilder.build()); 345d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mLastNotificationTime = currentTime; 346d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } 347d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } 348d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 349d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa /** 350d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * Generates an estimate of the remaining time in the copy. 351d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * 352d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * @param elapsedTime The time elapsed so far. 353d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa */ 354d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa private void updateRemainingTimeEstimate(long elapsedTime) { 355d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa final long sampleDuration = elapsedTime - mSampleTime; 356d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa final long sampleSpeed = ((mBytesCopied - mBytesCopiedSample) * 1000) / sampleDuration; 357d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa if (mSpeed == 0) { 358d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mSpeed = sampleSpeed; 359d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } else { 360d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mSpeed = ((3 * mSpeed) + sampleSpeed) / 4; 361d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } 362d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 363d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa if (mSampleTime > 0 && mSpeed > 0) { 364d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mRemainingTime = ((mBatchSize - mBytesCopied) * 1000) / mSpeed; 365d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } else { 366d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mRemainingTime = 0; 367d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } 368d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 369d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mSampleTime = elapsedTime; 370d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa mBytesCopiedSample = mBytesCopied; 371d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } 372d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 373d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa /** 374726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * Copies a the given documents to the given location. 375d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa * 376726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * @param srcInfo DocumentInfos for the documents to copy. 377e1a03f8eec2719279037ab348df306764dc45a70Tomasz Mikolajewski * @param dstDirInfo The destination directory. 378726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * @throws RemoteException 379d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa */ 380e1a03f8eec2719279037ab348df306764dc45a70Tomasz Mikolajewski private void copy(DocumentInfo srcInfo, DocumentInfo dstDirInfo) throws RemoteException { 381e1a03f8eec2719279037ab348df306764dc45a70Tomasz Mikolajewski final Uri dstUri = DocumentsContract.createDocument(mDstClient, dstDirInfo.derivedUri, 382ef3f2620b3a755856d70345fc7a90df896985c26Ben Kwa srcInfo.mimeType, srcInfo.displayName); 383726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa if (dstUri == null) { 384726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa // If this is a directory, the entire subdir will not be copied over. 385726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa Log.e(TAG, "Error while copying " + srcInfo.displayName); 386f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski mFailedFiles.add(srcInfo); 387726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa return; 388726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } 389726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa 390726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa if (Document.MIME_TYPE_DIR.equals(srcInfo.mimeType)) { 391726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa copyDirectoryHelper(srcInfo.derivedUri, dstUri); 392726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } else { 393726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa copyFileHelper(srcInfo.derivedUri, dstUri); 394726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } 395726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } 396ef3f2620b3a755856d70345fc7a90df896985c26Ben Kwa 397726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa /** 398726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * Handles recursion into a directory and copying its contents. Note that in linux terms, this 399726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * does the equivalent of "cp src/* dst", not "cp -r src dst". 400726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * 401726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * @param srcDirUri URI of the directory to copy from. The routine will copy the directory's 402726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * contents, not the directory itself. 403726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * @param dstDirUri URI of the directory to copy to. Must be created beforehand. 404726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * @throws RemoteException 405726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa */ 406726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa private void copyDirectoryHelper(Uri srcDirUri, Uri dstDirUri) throws RemoteException { 407726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa // Recurse into directories. Copy children into the new subdirectory. 408726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa final String queryColumns[] = new String[] { 409726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa Document.COLUMN_DISPLAY_NAME, 410726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa Document.COLUMN_DOCUMENT_ID, 411726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa Document.COLUMN_MIME_TYPE, 412726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa Document.COLUMN_SIZE 413726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa }; 414726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa final Uri queryUri = DocumentsContract.buildChildDocumentsUri(srcDirUri.getAuthority(), 415726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa DocumentsContract.getDocumentId(srcDirUri)); 416726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa Cursor cursor = null; 417726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa try { 418726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa // Iterate over srcs in the directory; copy to the destination directory. 419726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa cursor = mSrcClient.query(queryUri, queryColumns, null, null, null); 420726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa while (cursor.moveToNext()) { 421726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa final String childMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); 422726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa final Uri dstUri = DocumentsContract.createDocument(mDstClient, dstDirUri, 423726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa childMimeType, getCursorString(cursor, Document.COLUMN_DISPLAY_NAME)); 424726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa final Uri childUri = DocumentsContract.buildDocumentUri(srcDirUri.getAuthority(), 425726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa getCursorString(cursor, Document.COLUMN_DOCUMENT_ID)); 426726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa if (Document.MIME_TYPE_DIR.equals(childMimeType)) { 427726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa copyDirectoryHelper(childUri, dstUri); 428726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } else { 429726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa copyFileHelper(childUri, dstUri); 430726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } 431726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } 432726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } finally { 433726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa IoUtils.closeQuietly(cursor); 434726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } 435726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } 436726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa 437726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa /** 438726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * Handles copying a single file. 439726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * 440726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * @param srcUri URI of the file to copy from. 441726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * @param dstUri URI of the *file* to copy to. Must be created beforehand. 442726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa * @throws RemoteException 443726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa */ 444726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa private void copyFileHelper(Uri srcUri, Uri dstUri) throws RemoteException { 445726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa // Copy an individual file. 446ef3f2620b3a755856d70345fc7a90df896985c26Ben Kwa CancellationSignal canceller = new CancellationSignal(); 447ef3f2620b3a755856d70345fc7a90df896985c26Ben Kwa ParcelFileDescriptor srcFile = null; 448ef3f2620b3a755856d70345fc7a90df896985c26Ben Kwa ParcelFileDescriptor dstFile = null; 449ef3f2620b3a755856d70345fc7a90df896985c26Ben Kwa InputStream src = null; 450ef3f2620b3a755856d70345fc7a90df896985c26Ben Kwa OutputStream dst = null; 451d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 452c7a01cfe15b3f485c9f711d83c33d548ee120795Ben Kwa IOException copyError = null; 453d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa try { 454726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa srcFile = mSrcClient.openFile(srcUri, "r", canceller); 455726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa dstFile = mDstClient.openFile(dstUri, "w", canceller); 456ef3f2620b3a755856d70345fc7a90df896985c26Ben Kwa src = new ParcelFileDescriptor.AutoCloseInputStream(srcFile); 457ef3f2620b3a755856d70345fc7a90df896985c26Ben Kwa dst = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile); 458d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 459d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa byte[] buffer = new byte[8192]; 460d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa int len; 461ef3f2620b3a755856d70345fc7a90df896985c26Ben Kwa while (!mIsCancelled && ((len = src.read(buffer)) != -1)) { 462ef3f2620b3a755856d70345fc7a90df896985c26Ben Kwa dst.write(buffer, 0, len); 463d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa makeProgress(len); 464d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } 465c7a01cfe15b3f485c9f711d83c33d548ee120795Ben Kwa 466ef3f2620b3a755856d70345fc7a90df896985c26Ben Kwa srcFile.checkError(); 467d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } catch (IOException e) { 468c7a01cfe15b3f485c9f711d83c33d548ee120795Ben Kwa copyError = e; 46975597d0dc568efa29f6ef2aadbbb0f9986848bdaBen Kwa try { 47075597d0dc568efa29f6ef2aadbbb0f9986848bdaBen Kwa dstFile.closeWithError(copyError.getMessage()); 47175597d0dc568efa29f6ef2aadbbb0f9986848bdaBen Kwa } catch (IOException closeError) { 47275597d0dc568efa29f6ef2aadbbb0f9986848bdaBen Kwa Log.e(TAG, "Error closing destination", closeError); 473c7a01cfe15b3f485c9f711d83c33d548ee120795Ben Kwa } 47475597d0dc568efa29f6ef2aadbbb0f9986848bdaBen Kwa } finally { 475c7a01cfe15b3f485c9f711d83c33d548ee120795Ben Kwa // This also ensures the file descriptors are closed. 476c7a01cfe15b3f485c9f711d83c33d548ee120795Ben Kwa IoUtils.closeQuietly(src); 477c7a01cfe15b3f485c9f711d83c33d548ee120795Ben Kwa IoUtils.closeQuietly(dst); 478c7a01cfe15b3f485c9f711d83c33d548ee120795Ben Kwa } 479c7a01cfe15b3f485c9f711d83c33d548ee120795Ben Kwa 480c7a01cfe15b3f485c9f711d83c33d548ee120795Ben Kwa if (copyError != null) { 481c7a01cfe15b3f485c9f711d83c33d548ee120795Ben Kwa // Log errors. 482c7a01cfe15b3f485c9f711d83c33d548ee120795Ben Kwa Log.e(TAG, "Error while copying " + srcUri.toString(), copyError); 483f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski try { 484f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski mFailedFiles.add(DocumentInfo.fromUri(getContentResolver(), srcUri)); 485f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski } catch (FileNotFoundException ignore) { 486c7a01cfe15b3f485c9f711d83c33d548ee120795Ben Kwa Log.w(TAG, "Source file gone: " + srcUri, copyError); 487f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski // The source file is gone. 488f8c3f322a276c8f78b8918fddedfe6e011d4e24aTomasz Mikolajewski } 489d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } 490d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa 491c7a01cfe15b3f485c9f711d83c33d548ee120795Ben Kwa if (copyError != null || mIsCancelled) { 492d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa // Clean up half-copied files. 493ef3f2620b3a755856d70345fc7a90df896985c26Ben Kwa canceller.cancel(); 494726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa try { 495726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa DocumentsContract.deleteDocument(mDstClient, dstUri); 496726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa } catch (RemoteException e) { 497726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa Log.w(TAG, "Failed to clean up: " + srcUri, e); 498726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa // RemoteExceptions usually signal that the connection is dead, so there's no point 499726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa // attempting to continue. Propagate the exception up so the copy job is cancelled. 500726cf70fd4e436ba0ba5fff1a42e5c726a9786c5Ben Kwa throw e; 501d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } 502d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } 503d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa } 504d99109fca847895233b0bdfafa131ebca8dfe3d5Ben Kwa} 505