blob: cc9ab978295a211a364840a8f3203fab5189dfb9 [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;
20import android.content.ClipboardManager;
Steve McKay1f199482015-05-20 15:58:42 -070021import android.content.ContentResolver;
22import android.content.Context;
Steve McKay1f199482015-05-20 15:58:42 -070023import android.net.Uri;
Garfield, Tan4d009142016-05-12 14:12:18 -070024import android.os.AsyncTask;
Ben Linff4d5842016-04-18 14:35:28 -070025import android.os.PersistableBundle;
Steve McKay1f199482015-05-20 15:58:42 -070026import android.provider.DocumentsContract;
Steve McKayfefcd702015-08-20 16:19:38 +000027import android.support.annotation.Nullable;
Steve McKay1f199482015-05-20 15:58:42 -070028import android.util.Log;
29
30import com.android.documentsui.model.DocumentInfo;
Garfield, Tan4d009142016-05-12 14:12:18 -070031import com.android.documentsui.model.DocumentStack;
32import com.android.documentsui.model.RootInfo;
Ben Linff4d5842016-04-18 14:35:28 -070033import com.android.documentsui.services.FileOperationService;
34import com.android.documentsui.services.FileOperationService.OpType;
Garfield, Tan4d009142016-05-12 14:12:18 -070035import com.android.documentsui.services.FileOperations;
Steve McKay1f199482015-05-20 15:58:42 -070036
Steve McKayfefcd702015-08-20 16:19:38 +000037import java.util.ArrayList;
Vladislav Kaznacheev0859d302016-05-03 15:37:21 -070038import java.util.Arrays;
Steve McKay22b35a72016-04-13 14:03:24 -070039import java.util.Collections;
Vladislav Kaznacheev0859d302016-05-03 15:37:21 -070040import java.util.HashSet;
Steve McKay1f199482015-05-20 15:58:42 -070041import java.util.List;
42
43/**
44 * ClipboardManager wrapper class providing higher level logical
45 * support for dealing with Documents.
46 */
Steve McKayf68210e2015-11-03 15:23:16 -080047public final class DocumentClipper {
Steve McKay1f199482015-05-20 15:58:42 -070048
49 private static final String TAG = "DocumentClipper";
Ben Linff4d5842016-04-18 14:35:28 -070050 private static final String SRC_PARENT_KEY = "srcParent";
51 private static final String OP_TYPE_KEY = "opType";
Steve McKay1f199482015-05-20 15:58:42 -070052
53 private Context mContext;
54 private ClipboardManager mClipboard;
55
Garfield, Tan4d009142016-05-12 14:12:18 -070056 DocumentClipper(Context context) {
Steve McKay1f199482015-05-20 15:58:42 -070057 mContext = context;
58 mClipboard = context.getSystemService(ClipboardManager.class);
59 }
60
61 public boolean hasItemsToPaste() {
62 if (mClipboard.hasPrimaryClip()) {
63 ClipData clipData = mClipboard.getPrimaryClip();
64 int count = clipData.getItemCount();
65 if (count > 0) {
66 for (int i = 0; i < count; ++i) {
67 ClipData.Item item = clipData.getItemAt(i);
68 Uri uri = item.getUri();
69 if (isDocumentUri(uri)) {
70 return true;
71 }
72 }
73 }
74 }
75 return false;
76 }
77
78 private boolean isDocumentUri(@Nullable Uri uri) {
79 return uri != null && DocumentsContract.isDocumentUri(mContext, uri);
80 }
81
82 /**
Ben Linff4d5842016-04-18 14:35:28 -070083 * Returns details regarding the documents on the primary clipboard
Steve McKay1f199482015-05-20 15:58:42 -070084 */
Ben Linff4d5842016-04-18 14:35:28 -070085 public ClipDetails getClipDetails() {
86 return getClipDetails(mClipboard.getPrimaryClip());
Steve McKay1f199482015-05-20 15:58:42 -070087 }
88
Ben Linff4d5842016-04-18 14:35:28 -070089 public ClipDetails getClipDetails(@Nullable ClipData clipData) {
90 if (clipData == null) {
91 return null;
92 }
93
94 String srcParent = clipData.getDescription().getExtras().getString(SRC_PARENT_KEY);
95
96 ClipDetails clipDetails = new ClipDetails(
97 clipData.getDescription().getExtras().getInt(OP_TYPE_KEY),
98 getDocumentsFromClipData(clipData),
99 createDocument((srcParent != null) ? Uri.parse(srcParent) : null));
100
101 return clipDetails;
102 }
103
104 private List<DocumentInfo> getDocumentsFromClipData(ClipData clipData) {
Steve McKay0af8afd2016-02-25 13:34:03 -0800105 assert(clipData != null);
Steve McKay1f199482015-05-20 15:58:42 -0700106
107 int count = clipData.getItemCount();
108 if (count == 0) {
Ben Linff4d5842016-04-18 14:35:28 -0700109 return Collections.EMPTY_LIST;
Steve McKay1f199482015-05-20 15:58:42 -0700110 }
111
Ben Linff4d5842016-04-18 14:35:28 -0700112 final List<DocumentInfo> srcDocs = new ArrayList<>();
113
Steve McKay1f199482015-05-20 15:58:42 -0700114 for (int i = 0; i < count; ++i) {
115 ClipData.Item item = clipData.getItemAt(i);
116 Uri itemUri = item.getUri();
Garfield, Tan4d009142016-05-12 14:12:18 -0700117 DocumentInfo docInfo = createDocument(itemUri);
118 if (docInfo != null) {
119 srcDocs.add(docInfo);
120 } else {
121 // This uri either doesn't exist, or is invalid.
122 Log.w(TAG, "Can't create document info from uri: " + itemUri);
123 }
Steve McKay1f199482015-05-20 15:58:42 -0700124 }
125
126 return srcDocs;
127 }
128
129 /**
130 * Returns ClipData representing the list of docs, or null if docs is empty,
131 * or docs cannot be converted.
132 */
Ben Linff4d5842016-04-18 14:35:28 -0700133 public @Nullable ClipData getClipDataForDocuments(List<DocumentInfo> docs, @OpType int opType) {
Steve McKay1f199482015-05-20 15:58:42 -0700134 final ContentResolver resolver = mContext.getContentResolver();
Vladislav Kaznacheev0859d302016-05-03 15:37:21 -0700135 final String[] mimeTypes = getMimeTypes(resolver, docs);
Steve McKay1f199482015-05-20 15:58:42 -0700136 ClipData clipData = null;
137 for (DocumentInfo doc : docs) {
Ben Linff4d5842016-04-18 14:35:28 -0700138 assert(doc != null);
139 assert(doc.derivedUri != null);
Steve McKay1f199482015-05-20 15:58:42 -0700140 if (clipData == null) {
141 // TODO: figure out what this string should be.
142 // Currently it is not displayed anywhere in the UI, but this might change.
Ben Linff4d5842016-04-18 14:35:28 -0700143 final String clipLabel = "";
Vladislav Kaznacheev9c628d02016-05-04 10:56:05 -0700144 clipData = new ClipData(clipLabel, mimeTypes, new ClipData.Item(doc.derivedUri));
Ben Linff4d5842016-04-18 14:35:28 -0700145 PersistableBundle bundle = new PersistableBundle();
146 bundle.putInt(OP_TYPE_KEY, opType);
147 clipData.getDescription().setExtras(bundle);
Steve McKay1f199482015-05-20 15:58:42 -0700148 } else {
149 // TODO: update list of mime types in ClipData.
Ben Linff4d5842016-04-18 14:35:28 -0700150 clipData.addItem(new ClipData.Item(doc.derivedUri));
Steve McKay1f199482015-05-20 15:58:42 -0700151 }
152 }
153 return clipData;
154 }
155
Vladislav Kaznacheev0859d302016-05-03 15:37:21 -0700156 private static String[] getMimeTypes(ContentResolver resolver, List<DocumentInfo> docs) {
157 final HashSet<String> mimeTypes = new HashSet<>();
158 for (DocumentInfo doc : docs) {
159 assert(doc != null);
160 assert(doc.derivedUri != null);
161 final Uri uri = doc.derivedUri;
162 if ("content".equals(uri.getScheme())) {
163 mimeTypes.add(resolver.getType(uri));
164 final String[] streamTypes = resolver.getStreamTypes(uri, "*/*");
165 if (streamTypes != null) {
166 mimeTypes.addAll(Arrays.asList(streamTypes));
167 }
168 }
169 }
170 return mimeTypes.toArray(new String[0]);
171 }
172
Ben Linff4d5842016-04-18 14:35:28 -0700173 /**
174 * Puts {@code ClipData} in a primary clipboard, describing a copy operation
175 */
176 public void clipDocumentsForCopy(List<DocumentInfo> docs) {
177 ClipData data = getClipDataForDocuments(docs, FileOperationService.OPERATION_COPY);
178 assert(data != null);
179
Steve McKay1f199482015-05-20 15:58:42 -0700180 mClipboard.setPrimaryClip(data);
181 }
Ben Linff4d5842016-04-18 14:35:28 -0700182
183 /**
184 * Puts {@Code ClipData} in a primary clipboard, describing a cut operation
185 */
186 public void clipDocumentsForCut(List<DocumentInfo> docs, DocumentInfo srcParent) {
187 assert(docs != null);
188 assert(!docs.isEmpty());
189 assert(srcParent != null);
190 assert(srcParent.derivedUri != null);
191
192 ClipData data = getClipDataForDocuments(docs, FileOperationService.OPERATION_MOVE);
193 assert(data != null);
194
195 PersistableBundle bundle = data.getDescription().getExtras();
196 bundle.putString(SRC_PARENT_KEY, srcParent.derivedUri.toString());
197
198 mClipboard.setPrimaryClip(data);
199 }
200
201 private DocumentInfo createDocument(Uri uri) {
202 DocumentInfo doc = null;
203 if (uri != null && DocumentsContract.isDocumentUri(mContext, uri)) {
204 ContentResolver resolver = mContext.getContentResolver();
Ben Linff4d5842016-04-18 14:35:28 -0700205 try {
Garfield, Tan4d009142016-05-12 14:12:18 -0700206 doc = DocumentInfo.fromUri(resolver, uri);
Ben Linff4d5842016-04-18 14:35:28 -0700207 } catch (Exception e) {
208 Log.e(TAG, e.getMessage());
Ben Linff4d5842016-04-18 14:35:28 -0700209 }
210 }
211 return doc;
212 }
213
Garfield, Tan4d009142016-05-12 14:12:18 -0700214 /**
215 * Copies documents from clipboard. It's the same as {@link #copyFromClipData} with clipData
216 * returned from {@link ClipboardManager#getPrimaryClip()}.
217 *
218 * @param destination destination document.
219 * @param docStack the document stack to the destination folder,
220 * @param callback callback to notify when operation finishes.
221 */
222 public void copyFromClipboard(DocumentInfo destination, DocumentStack docStack,
223 FileOperations.Callback callback) {
224 copyFromClipData(destination, docStack, mClipboard.getPrimaryClip(), callback);
225 }
226
227 /**
228 * Copies documents from given clip data.
229 *
230 * @param destination destination document
231 * @param docStack the document stack to the destination folder
232 * @param clipData the clipData to copy from, or null to copy from clipboard
233 * @param callback callback to notify when operation finishes
234 */
235 public void copyFromClipData(final DocumentInfo destination, DocumentStack docStack,
236 @Nullable final ClipData clipData, final FileOperations.Callback callback) {
237 if (clipData == null) {
238 Log.i(TAG, "Received null clipData. Ignoring.");
239 return;
240 }
241
242 new AsyncTask<Void, Void, ClipDetails>() {
243
244 @Override
245 protected ClipDetails doInBackground(Void... params) {
246 return getClipDetails(clipData);
247 }
248
249 @Override
250 protected void onPostExecute(ClipDetails clipDetails) {
251 if (clipDetails == null) {
252 Log.w(TAG, "Received null clipDetails. Ignoring.");
253 return;
254 }
255
256 List<DocumentInfo> docs = clipDetails.docs;
257 @OpType int type = clipDetails.opType;
258 DocumentInfo srcParent = clipDetails.parent;
259 moveDocuments(docs, destination, docStack, type, srcParent, callback);
260 }
261 }.execute();
262 }
263
264 /**
265 * Moves {@code docs} from {@code srcParent} to {@code destination}.
266 * operationType can be copy or cut
267 * srcParent Must be non-null for move operations.
268 */
269 private void moveDocuments(
270 List<DocumentInfo> docs,
271 DocumentInfo destination,
272 DocumentStack docStack,
273 @OpType int operationType,
274 DocumentInfo srcParent,
275 FileOperations.Callback callback) {
276
277 RootInfo destRoot = docStack.root;
278 if (!canCopy(docs, destRoot, destination)) {
279 callback.onOperationResult(FileOperations.Callback.STATUS_REJECTED, operationType, 0);
280 return;
281 }
282
283 if (docs.isEmpty()) {
284 callback.onOperationResult(FileOperations.Callback.STATUS_ACCEPTED, operationType, 0);
285 return;
286 }
287
288 DocumentStack dstStack = new DocumentStack();
289 dstStack.push(destination);
290 dstStack.addAll(docStack);
291 switch (operationType) {
292 case FileOperationService.OPERATION_MOVE:
293 FileOperations.move(mContext, docs, srcParent, dstStack, callback);
294 break;
295 case FileOperationService.OPERATION_COPY:
296 FileOperations.copy(mContext, docs, dstStack, callback);
297 break;
298 default:
299 throw new UnsupportedOperationException("Unsupported operation: " + operationType);
300 }
301 }
302
303 /**
304 * Returns true if the list of files can be copied to destination. Note that this
305 * is a policy check only. Currently the method does not attempt to verify
306 * available space or any other environmental aspects possibly resulting in
307 * failure to copy.
308 *
309 * @return true if the list of files can be copied to destination.
310 */
311 private boolean canCopy(List<DocumentInfo> files, RootInfo root, DocumentInfo dest) {
312 if (dest == null || !dest.isDirectory() || !dest.isCreateSupported()) {
313 return false;
314 }
315
316 // Can't copy folders to downloads, because we don't show folders there.
317 if (root.isDownloads()) {
318 for (DocumentInfo docs : files) {
319 if (docs.isDirectory()) {
320 return false;
321 }
322 }
323 }
324
325 return true;
326 }
327
Ben Linff4d5842016-04-18 14:35:28 -0700328 public static class ClipDetails {
329 public final @OpType int opType;
330 public final List<DocumentInfo> docs;
331 public final @Nullable DocumentInfo parent;
332
333 ClipDetails(@OpType int opType, List<DocumentInfo> docs, @Nullable DocumentInfo parent) {
334 this.opType = opType;
335 this.docs = docs;
336 this.parent = parent;
337 }
338 }
Steve McKay1f199482015-05-20 15:58:42 -0700339}