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