blob: 3d8ac2c936a6593cfe369fdfd5a111aba2932564 [file] [log] [blame]
Steve McKay1f199482015-05-20 15:58:42 -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 McKay1f199482015-05-20 15:58:42 -070019import android.content.ClipData;
Steve McKay84769b82016-06-09 10:46:07 -070020import android.content.ClipDescription;
Steve McKay1f199482015-05-20 15:58:42 -070021import android.content.ClipboardManager;
Steve McKay1f199482015-05-20 15:58:42 -070022import android.content.ContentResolver;
23import android.content.Context;
Steve McKay1f199482015-05-20 15:58:42 -070024import android.net.Uri;
Garfield, Tan4d009142016-05-12 14:12:18 -070025import android.os.AsyncTask;
Ben Linff4d5842016-04-18 14:35:28 -070026import android.os.PersistableBundle;
Steve McKay1f199482015-05-20 15:58:42 -070027import android.provider.DocumentsContract;
Steve McKayfefcd702015-08-20 16:19:38 +000028import android.support.annotation.Nullable;
Steve McKay1f199482015-05-20 15:58:42 -070029import android.util.Log;
30
Steve McKay84769b82016-06-09 10:46:07 -070031import com.android.documentsui.ClipStorage.Writer;
32import com.android.documentsui.dirlist.MultiSelectManager.Selection;
Steve McKay1f199482015-05-20 15:58:42 -070033import com.android.documentsui.model.DocumentInfo;
Garfield, Tan4d009142016-05-12 14:12:18 -070034import com.android.documentsui.model.DocumentStack;
35import com.android.documentsui.model.RootInfo;
Ben Linff4d5842016-04-18 14:35:28 -070036import com.android.documentsui.services.FileOperationService;
37import com.android.documentsui.services.FileOperationService.OpType;
Garfield, Tan4d009142016-05-12 14:12:18 -070038import com.android.documentsui.services.FileOperations;
Steve McKay1f199482015-05-20 15:58:42 -070039
Steve McKay84769b82016-06-09 10:46:07 -070040import java.io.IOException;
Steve McKayfefcd702015-08-20 16:19:38 +000041import java.util.ArrayList;
Steve McKay22b35a72016-04-13 14:03:24 -070042import java.util.Collections;
Vladislav Kaznacheev0859d302016-05-03 15:37:21 -070043import java.util.HashSet;
Steve McKay1f199482015-05-20 15:58:42 -070044import java.util.List;
Steve McKay84769b82016-06-09 10:46:07 -070045import java.util.Set;
46import java.util.function.Function;
Steve McKay1f199482015-05-20 15:58:42 -070047
48/**
49 * ClipboardManager wrapper class providing higher level logical
50 * support for dealing with Documents.
51 */
Steve McKayf68210e2015-11-03 15:23:16 -080052public final class DocumentClipper {
Steve McKay1f199482015-05-20 15:58:42 -070053
54 private static final String TAG = "DocumentClipper";
Ben Linff4d5842016-04-18 14:35:28 -070055 private static final String SRC_PARENT_KEY = "srcParent";
56 private static final String OP_TYPE_KEY = "opType";
Steve McKay84769b82016-06-09 10:46:07 -070057 private static final String OP_JUMBO_SELECTION_SIZE = "jumboSelection-size";
Steve McKay1f199482015-05-20 15:58:42 -070058
Steve McKay84769b82016-06-09 10:46:07 -070059 private final Context mContext;
60 private final ClipStorage mClipStorage;
61 private final ClipboardManager mClipboard;
Steve McKay1f199482015-05-20 15:58:42 -070062
Steve McKay84769b82016-06-09 10:46:07 -070063 DocumentClipper(Context context, ClipStorage storage) {
Steve McKay1f199482015-05-20 15:58:42 -070064 mContext = context;
Steve McKay84769b82016-06-09 10:46:07 -070065 mClipStorage = storage;
Steve McKay1f199482015-05-20 15:58:42 -070066 mClipboard = context.getSystemService(ClipboardManager.class);
67 }
68
69 public boolean hasItemsToPaste() {
70 if (mClipboard.hasPrimaryClip()) {
71 ClipData clipData = mClipboard.getPrimaryClip();
72 int count = clipData.getItemCount();
73 if (count > 0) {
74 for (int i = 0; i < count; ++i) {
75 ClipData.Item item = clipData.getItemAt(i);
76 Uri uri = item.getUri();
77 if (isDocumentUri(uri)) {
78 return true;
79 }
80 }
81 }
82 }
83 return false;
84 }
85
86 private boolean isDocumentUri(@Nullable Uri uri) {
87 return uri != null && DocumentsContract.isDocumentUri(mContext, uri);
88 }
89
Ben Linff4d5842016-04-18 14:35:28 -070090 public ClipDetails getClipDetails(@Nullable ClipData clipData) {
91 if (clipData == null) {
92 return null;
93 }
94
95 String srcParent = clipData.getDescription().getExtras().getString(SRC_PARENT_KEY);
96
97 ClipDetails clipDetails = new ClipDetails(
98 clipData.getDescription().getExtras().getInt(OP_TYPE_KEY),
99 getDocumentsFromClipData(clipData),
100 createDocument((srcParent != null) ? Uri.parse(srcParent) : null));
101
102 return clipDetails;
103 }
104
105 private List<DocumentInfo> getDocumentsFromClipData(ClipData clipData) {
Steve McKay0af8afd2016-02-25 13:34:03 -0800106 assert(clipData != null);
Steve McKay1f199482015-05-20 15:58:42 -0700107
108 int count = clipData.getItemCount();
109 if (count == 0) {
Ben Linff4d5842016-04-18 14:35:28 -0700110 return Collections.EMPTY_LIST;
Steve McKay1f199482015-05-20 15:58:42 -0700111 }
112
Ben Linff4d5842016-04-18 14:35:28 -0700113 final List<DocumentInfo> srcDocs = new ArrayList<>();
114
Steve McKay1f199482015-05-20 15:58:42 -0700115 for (int i = 0; i < count; ++i) {
116 ClipData.Item item = clipData.getItemAt(i);
117 Uri itemUri = item.getUri();
Garfield, Tan4d009142016-05-12 14:12:18 -0700118 DocumentInfo docInfo = createDocument(itemUri);
119 if (docInfo != null) {
120 srcDocs.add(docInfo);
121 } else {
122 // This uri either doesn't exist, or is invalid.
123 Log.w(TAG, "Can't create document info from uri: " + itemUri);
124 }
Steve McKay1f199482015-05-20 15:58:42 -0700125 }
126
127 return srcDocs;
128 }
129
130 /**
Steve McKay84769b82016-06-09 10:46:07 -0700131 * Returns {@link ClipData} representing the selection, or null if selection is empty,
132 * or cannot be converted.
133 */
134 public @Nullable ClipData getClipDataForDocuments(
135 Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
136
137 assert(selection != null);
138
139 if (selection.isEmpty()) {
140 Log.w(TAG, "Attempting to clip empty selection. Ignoring.");
141 return null;
142 }
143
144 return (selection.size() > Shared.MAX_DOCS_IN_INTENT)
145 ? createJumboClipData(uriBuilder, selection, opType)
146 : createStandardClipData(uriBuilder, selection, opType);
147 }
148
149 /**
150 * Returns ClipData representing the selection.
151 */
152 private @Nullable ClipData createStandardClipData(
153 Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
154
155 assert(!selection.isEmpty());
156
157 final ContentResolver resolver = mContext.getContentResolver();
158 final ArrayList<ClipData.Item> clipItems = new ArrayList<>();
159 final Set<String> clipTypes = new HashSet<>();
160
161 PersistableBundle bundle = new PersistableBundle();
162 bundle.putInt(OP_TYPE_KEY, opType);
163
164 int clipCount = 0;
165 for (String id : selection) {
166 assert(id != null);
167 Uri uri = uriBuilder.apply(id);
168 if (clipCount <= Shared.MAX_DOCS_IN_INTENT) {
169 DocumentInfo.addMimeTypes(resolver, uri, clipTypes);
170 clipItems.add(new ClipData.Item(uri));
171 }
172 clipCount++;
173 }
174
175 ClipDescription description = new ClipDescription(
176 "", // Currently "label" is not displayed anywhere in the UI.
177 clipTypes.toArray(new String[0]));
178 description.setExtras(bundle);
179
180 return new ClipData(description, clipItems);
181 }
182
183 /**
Steve McKay1f199482015-05-20 15:58:42 -0700184 * Returns ClipData representing the list of docs, or null if docs is empty,
185 * or docs cannot be converted.
186 */
Steve McKay84769b82016-06-09 10:46:07 -0700187 private @Nullable ClipData createJumboClipData(
188 Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
Steve McKay1f199482015-05-20 15:58:42 -0700189
Steve McKay84769b82016-06-09 10:46:07 -0700190 assert(!selection.isEmpty());
191
192 final ContentResolver resolver = mContext.getContentResolver();
193 final ArrayList<ClipData.Item> clipItems = new ArrayList<>();
194 final Set<String> clipTypes = new HashSet<>();
195
196 PersistableBundle bundle = new PersistableBundle();
197 bundle.putInt(OP_TYPE_KEY, opType);
198 bundle.putInt(OP_JUMBO_SELECTION_SIZE, selection.size());
199
200 int clipCount = 0;
201 synchronized (mClipStorage) {
202 try (Writer writer = mClipStorage.createWriter()) {
203 for (String id : selection) {
204 assert(id != null);
205 Uri uri = uriBuilder.apply(id);
206 if (clipCount <= Shared.MAX_DOCS_IN_INTENT) {
207 DocumentInfo.addMimeTypes(resolver, uri, clipTypes);
208 clipItems.add(new ClipData.Item(uri));
209 }
210 writer.write(uri);
211 clipCount++;
Vladislav Kaznacheev0859d302016-05-03 15:37:21 -0700212 }
Steve McKay84769b82016-06-09 10:46:07 -0700213 } catch (IOException e) {
214 Log.e(TAG, "Caught exception trying to write jumbo clip to disk.", e);
215 return null;
Vladislav Kaznacheev0859d302016-05-03 15:37:21 -0700216 }
217 }
Steve McKay84769b82016-06-09 10:46:07 -0700218
219 ClipDescription description = new ClipDescription(
220 "", // Currently "label" is not displayed anywhere in the UI.
221 clipTypes.toArray(new String[0]));
222 description.setExtras(bundle);
223
224 return new ClipData(description, clipItems);
Vladislav Kaznacheev0859d302016-05-03 15:37:21 -0700225 }
226
Ben Linff4d5842016-04-18 14:35:28 -0700227 /**
228 * Puts {@code ClipData} in a primary clipboard, describing a copy operation
229 */
Steve McKay84769b82016-06-09 10:46:07 -0700230 public void clipDocumentsForCopy(Function<String, Uri> uriBuilder, Selection selection) {
231 ClipData data =
232 getClipDataForDocuments(uriBuilder, selection, FileOperationService.OPERATION_COPY);
Ben Linff4d5842016-04-18 14:35:28 -0700233 assert(data != null);
234
Steve McKay1f199482015-05-20 15:58:42 -0700235 mClipboard.setPrimaryClip(data);
236 }
Ben Linff4d5842016-04-18 14:35:28 -0700237
238 /**
239 * Puts {@Code ClipData} in a primary clipboard, describing a cut operation
240 */
Steve McKay84769b82016-06-09 10:46:07 -0700241 public void clipDocumentsForCut(
242 Function<String, Uri> uriBuilder, Selection selection, DocumentInfo parent) {
243 assert(!selection.isEmpty());
244 assert(parent.derivedUri != null);
Ben Linff4d5842016-04-18 14:35:28 -0700245
Steve McKay84769b82016-06-09 10:46:07 -0700246 ClipData data = getClipDataForDocuments(uriBuilder, selection,
247 FileOperationService.OPERATION_MOVE);
Ben Linff4d5842016-04-18 14:35:28 -0700248 assert(data != null);
249
250 PersistableBundle bundle = data.getDescription().getExtras();
Steve McKay84769b82016-06-09 10:46:07 -0700251 bundle.putString(SRC_PARENT_KEY, parent.derivedUri.toString());
Ben Linff4d5842016-04-18 14:35:28 -0700252
253 mClipboard.setPrimaryClip(data);
254 }
255
256 private DocumentInfo createDocument(Uri uri) {
257 DocumentInfo doc = null;
Steve McKay84769b82016-06-09 10:46:07 -0700258 if (isDocumentUri(uri)) {
Ben Linff4d5842016-04-18 14:35:28 -0700259 ContentResolver resolver = mContext.getContentResolver();
Ben Linff4d5842016-04-18 14:35:28 -0700260 try {
Garfield, Tan4d009142016-05-12 14:12:18 -0700261 doc = DocumentInfo.fromUri(resolver, uri);
Ben Linff4d5842016-04-18 14:35:28 -0700262 } catch (Exception e) {
263 Log.e(TAG, e.getMessage());
Ben Linff4d5842016-04-18 14:35:28 -0700264 }
265 }
266 return doc;
267 }
268
Garfield, Tan4d009142016-05-12 14:12:18 -0700269 /**
270 * Copies documents from clipboard. It's the same as {@link #copyFromClipData} with clipData
271 * returned from {@link ClipboardManager#getPrimaryClip()}.
272 *
273 * @param destination destination document.
274 * @param docStack the document stack to the destination folder,
275 * @param callback callback to notify when operation finishes.
276 */
Steve McKay84769b82016-06-09 10:46:07 -0700277 public void copyFromClipboard(
278 DocumentInfo destination,
279 DocumentStack docStack,
Garfield, Tan4d009142016-05-12 14:12:18 -0700280 FileOperations.Callback callback) {
Steve McKay84769b82016-06-09 10:46:07 -0700281
Garfield, Tan4d009142016-05-12 14:12:18 -0700282 copyFromClipData(destination, docStack, mClipboard.getPrimaryClip(), callback);
283 }
284
285 /**
286 * Copies documents from given clip data.
287 *
288 * @param destination destination document
289 * @param docStack the document stack to the destination folder
290 * @param clipData the clipData to copy from, or null to copy from clipboard
291 * @param callback callback to notify when operation finishes
292 */
Steve McKay84769b82016-06-09 10:46:07 -0700293 public void copyFromClipData(
294 final DocumentInfo destination,
295 DocumentStack docStack,
296 final @Nullable ClipData clipData,
297 final FileOperations.Callback callback) {
298
Garfield, Tan4d009142016-05-12 14:12:18 -0700299 if (clipData == null) {
300 Log.i(TAG, "Received null clipData. Ignoring.");
301 return;
302 }
303
304 new AsyncTask<Void, Void, ClipDetails>() {
305
306 @Override
307 protected ClipDetails doInBackground(Void... params) {
308 return getClipDetails(clipData);
309 }
310
311 @Override
312 protected void onPostExecute(ClipDetails clipDetails) {
313 if (clipDetails == null) {
314 Log.w(TAG, "Received null clipDetails. Ignoring.");
315 return;
316 }
317
318 List<DocumentInfo> docs = clipDetails.docs;
319 @OpType int type = clipDetails.opType;
320 DocumentInfo srcParent = clipDetails.parent;
321 moveDocuments(docs, destination, docStack, type, srcParent, callback);
322 }
323 }.execute();
324 }
325
326 /**
327 * Moves {@code docs} from {@code srcParent} to {@code destination}.
328 * operationType can be copy or cut
329 * srcParent Must be non-null for move operations.
330 */
331 private void moveDocuments(
332 List<DocumentInfo> docs,
333 DocumentInfo destination,
334 DocumentStack docStack,
335 @OpType int operationType,
336 DocumentInfo srcParent,
337 FileOperations.Callback callback) {
338
339 RootInfo destRoot = docStack.root;
340 if (!canCopy(docs, destRoot, destination)) {
341 callback.onOperationResult(FileOperations.Callback.STATUS_REJECTED, operationType, 0);
342 return;
343 }
344
345 if (docs.isEmpty()) {
346 callback.onOperationResult(FileOperations.Callback.STATUS_ACCEPTED, operationType, 0);
347 return;
348 }
349
350 DocumentStack dstStack = new DocumentStack();
351 dstStack.push(destination);
352 dstStack.addAll(docStack);
353 switch (operationType) {
354 case FileOperationService.OPERATION_MOVE:
355 FileOperations.move(mContext, docs, srcParent, dstStack, callback);
356 break;
357 case FileOperationService.OPERATION_COPY:
358 FileOperations.copy(mContext, docs, dstStack, callback);
359 break;
360 default:
361 throw new UnsupportedOperationException("Unsupported operation: " + operationType);
362 }
363 }
364
365 /**
366 * Returns true if the list of files can be copied to destination. Note that this
367 * is a policy check only. Currently the method does not attempt to verify
368 * available space or any other environmental aspects possibly resulting in
369 * failure to copy.
370 *
371 * @return true if the list of files can be copied to destination.
372 */
Steve McKay84769b82016-06-09 10:46:07 -0700373 private static boolean canCopy(List<DocumentInfo> files, RootInfo root, DocumentInfo dest) {
Garfield, Tan4d009142016-05-12 14:12:18 -0700374 if (dest == null || !dest.isDirectory() || !dest.isCreateSupported()) {
375 return false;
376 }
377
378 // Can't copy folders to downloads, because we don't show folders there.
379 if (root.isDownloads()) {
380 for (DocumentInfo docs : files) {
381 if (docs.isDirectory()) {
382 return false;
383 }
384 }
385 }
386
387 return true;
388 }
389
Ben Linff4d5842016-04-18 14:35:28 -0700390 public static class ClipDetails {
391 public final @OpType int opType;
392 public final List<DocumentInfo> docs;
393 public final @Nullable DocumentInfo parent;
394
395 ClipDetails(@OpType int opType, List<DocumentInfo> docs, @Nullable DocumentInfo parent) {
396 this.opType = opType;
397 this.docs = docs;
398 this.parent = parent;
399 }
400 }
Steve McKay1f199482015-05-20 15:58:42 -0700401}