blob: 4c103c42c2d57eeaee684efafc9bfd526af3539e [file] [log] [blame]
Steve McKaybdbd0ff2015-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 McKaybdbd0ff2015-05-20 15:58:42 -070019import android.content.ClipData;
Steve McKayd8187962016-06-09 10:46:07 -070020import android.content.ClipDescription;
Steve McKaybdbd0ff2015-05-20 15:58:42 -070021import android.content.ClipboardManager;
Steve McKaybdbd0ff2015-05-20 15:58:42 -070022import android.content.ContentResolver;
23import android.content.Context;
Garfield, Tanf46958b2016-06-17 15:32:28 -070024import android.content.SharedPreferences;
Steve McKaybdbd0ff2015-05-20 15:58:42 -070025import android.net.Uri;
Garfield, Tanf46958b2016-06-17 15:32:28 -070026import android.os.BaseBundle;
Ben Linb1404b72016-04-18 14:35:28 -070027import android.os.PersistableBundle;
Steve McKaybdbd0ff2015-05-20 15:58:42 -070028import android.provider.DocumentsContract;
Steve McKay58efce32015-08-20 16:19:38 +000029import android.support.annotation.Nullable;
Steve McKaybdbd0ff2015-05-20 15:58:42 -070030import android.util.Log;
31
Steve McKayd8187962016-06-09 10:46:07 -070032import com.android.documentsui.dirlist.MultiSelectManager.Selection;
Steve McKaybdbd0ff2015-05-20 15:58:42 -070033import com.android.documentsui.model.DocumentInfo;
Garfield, Tan6d0b46e2016-05-12 14:12:18 -070034import com.android.documentsui.model.DocumentStack;
Ben Linb1404b72016-04-18 14:35:28 -070035import com.android.documentsui.services.FileOperationService;
36import com.android.documentsui.services.FileOperationService.OpType;
Garfield, Tan6d0b46e2016-05-12 14:12:18 -070037import com.android.documentsui.services.FileOperations;
Steve McKaybdbd0ff2015-05-20 15:58:42 -070038
Steve McKayd8187962016-06-09 10:46:07 -070039import java.io.IOException;
Steve McKay58efce32015-08-20 16:19:38 +000040import java.util.ArrayList;
Vladislav Kaznacheev067d6fa2016-05-03 15:37:21 -070041import java.util.HashSet;
Steve McKaybdbd0ff2015-05-20 15:58:42 -070042import java.util.List;
Steve McKayd8187962016-06-09 10:46:07 -070043import java.util.Set;
44import java.util.function.Function;
Steve McKaybdbd0ff2015-05-20 15:58:42 -070045
46/**
47 * ClipboardManager wrapper class providing higher level logical
48 * support for dealing with Documents.
49 */
Garfield, Tanf46958b2016-06-17 15:32:28 -070050public final class DocumentClipper implements ClipboardManager.OnPrimaryClipChangedListener {
Steve McKaybdbd0ff2015-05-20 15:58:42 -070051
52 private static final String TAG = "DocumentClipper";
Garfield, Tanf46958b2016-06-17 15:32:28 -070053
54 static final String SRC_PARENT_KEY = "srcParent";
55 static final String OP_TYPE_KEY = "opType";
56 static final String OP_JUMBO_SELECTION_SIZE = "jumboSelection-size";
57 static final String OP_JUMBO_SELECTION_TAG = "jumboSelection-tag";
58
59 // Use shared preference to store last seen primary clip tag, so that we can delete the file
60 // when we realize primary clip has been changed when we're not running.
61 private static final String PREF_NAME = "DocumentClipperPref";
62 private static final String LAST_PRIMARY_CLIP_TAG = "lastPrimaryClipTag";
Steve McKaybdbd0ff2015-05-20 15:58:42 -070063
Steve McKayd8187962016-06-09 10:46:07 -070064 private final Context mContext;
65 private final ClipStorage mClipStorage;
66 private final ClipboardManager mClipboard;
Steve McKaybdbd0ff2015-05-20 15:58:42 -070067
Garfield, Tanf46958b2016-06-17 15:32:28 -070068 // Here we're tracking the last clipped tag ids so we can delete them later.
69 private long mLastDragClipTag = ClipStorage.NO_SELECTION_TAG;
70 private long mLastUnusedPrimaryClipTag = ClipStorage.NO_SELECTION_TAG;
71
72 private final SharedPreferences mPref;
73
Steve McKayd8187962016-06-09 10:46:07 -070074 DocumentClipper(Context context, ClipStorage storage) {
Steve McKaybdbd0ff2015-05-20 15:58:42 -070075 mContext = context;
Steve McKayd8187962016-06-09 10:46:07 -070076 mClipStorage = storage;
Steve McKaybdbd0ff2015-05-20 15:58:42 -070077 mClipboard = context.getSystemService(ClipboardManager.class);
Garfield, Tanf46958b2016-06-17 15:32:28 -070078
79 mClipboard.addPrimaryClipChangedListener(this);
80
81 // Primary clips may be changed when we're not running, now it's time to clean up the
82 // remnant.
83 mPref = context.getSharedPreferences(PREF_NAME, 0);
84 mLastUnusedPrimaryClipTag =
85 mPref.getLong(LAST_PRIMARY_CLIP_TAG, ClipStorage.NO_SELECTION_TAG);
86 deleteLastUnusedPrimaryClip();
Steve McKaybdbd0ff2015-05-20 15:58:42 -070087 }
88
89 public boolean hasItemsToPaste() {
90 if (mClipboard.hasPrimaryClip()) {
91 ClipData clipData = mClipboard.getPrimaryClip();
Garfield, Tanf46958b2016-06-17 15:32:28 -070092
Steve McKaybdbd0ff2015-05-20 15:58:42 -070093 int count = clipData.getItemCount();
94 if (count > 0) {
95 for (int i = 0; i < count; ++i) {
96 ClipData.Item item = clipData.getItemAt(i);
97 Uri uri = item.getUri();
98 if (isDocumentUri(uri)) {
99 return true;
100 }
101 }
102 }
103 }
104 return false;
105 }
106
107 private boolean isDocumentUri(@Nullable Uri uri) {
108 return uri != null && DocumentsContract.isDocumentUri(mContext, uri);
109 }
110
Garfield, Tanf46958b2016-06-17 15:32:28 -0700111 /**
112 * Returns {@link ClipData} representing the selection, or null if selection is empty,
113 * or cannot be converted.
114 *
115 * This is specialized for drag and drop so that we know which file to delete if nobody accepts
116 * the drop.
117 */
118 public @Nullable ClipData getClipDataForDrag(
119 Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
120 ClipData data = getClipDataForDocuments(uriBuilder, selection, opType);
Ben Linb1404b72016-04-18 14:35:28 -0700121
Garfield, Tanf46958b2016-06-17 15:32:28 -0700122 mLastDragClipTag = getTag(data);
Ben Linb1404b72016-04-18 14:35:28 -0700123
Garfield, Tanf46958b2016-06-17 15:32:28 -0700124 return data;
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700125 }
126
127 /**
Steve McKayd8187962016-06-09 10:46:07 -0700128 * Returns {@link ClipData} representing the selection, or null if selection is empty,
129 * or cannot be converted.
130 */
Garfield, Tanf46958b2016-06-17 15:32:28 -0700131 private @Nullable ClipData getClipDataForDocuments(
Steve McKayd8187962016-06-09 10:46:07 -0700132 Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
133
134 assert(selection != null);
135
136 if (selection.isEmpty()) {
137 Log.w(TAG, "Attempting to clip empty selection. Ignoring.");
138 return null;
139 }
140
141 return (selection.size() > Shared.MAX_DOCS_IN_INTENT)
142 ? createJumboClipData(uriBuilder, selection, opType)
143 : createStandardClipData(uriBuilder, selection, opType);
144 }
145
146 /**
147 * Returns ClipData representing the selection.
148 */
149 private @Nullable ClipData createStandardClipData(
150 Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
151
152 assert(!selection.isEmpty());
Garfield, Tanf46958b2016-06-17 15:32:28 -0700153 assert(selection.size() <= Shared.MAX_DOCS_IN_INTENT);
Steve McKayd8187962016-06-09 10:46:07 -0700154
155 final ContentResolver resolver = mContext.getContentResolver();
156 final ArrayList<ClipData.Item> clipItems = new ArrayList<>();
157 final Set<String> clipTypes = new HashSet<>();
158
159 PersistableBundle bundle = new PersistableBundle();
160 bundle.putInt(OP_TYPE_KEY, opType);
161
Steve McKayd8187962016-06-09 10:46:07 -0700162 for (String id : selection) {
163 assert(id != null);
164 Uri uri = uriBuilder.apply(id);
Garfield, Tanf46958b2016-06-17 15:32:28 -0700165 DocumentInfo.addMimeTypes(resolver, uri, clipTypes);
166 clipItems.add(new ClipData.Item(uri));
Steve McKayd8187962016-06-09 10:46:07 -0700167 }
168
169 ClipDescription description = new ClipDescription(
170 "", // Currently "label" is not displayed anywhere in the UI.
171 clipTypes.toArray(new String[0]));
172 description.setExtras(bundle);
173
174 return new ClipData(description, clipItems);
175 }
176
177 /**
Garfield, Tanf46958b2016-06-17 15:32:28 -0700178 * Returns ClipData representing the list of docs
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700179 */
Steve McKayd8187962016-06-09 10:46:07 -0700180 private @Nullable ClipData createJumboClipData(
181 Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700182
Steve McKayd8187962016-06-09 10:46:07 -0700183 assert(!selection.isEmpty());
Garfield, Tanf46958b2016-06-17 15:32:28 -0700184 assert(selection.size() > Shared.MAX_DOCS_IN_INTENT);
Steve McKayd8187962016-06-09 10:46:07 -0700185
Garfield, Tanf46958b2016-06-17 15:32:28 -0700186 final List<Uri> uris = new ArrayList<>(selection.size());
187
188 final int capacity = Math.min(selection.size(), Shared.MAX_DOCS_IN_INTENT);
189 final ArrayList<ClipData.Item> clipItems = new ArrayList<>(capacity);
190
191 // Set up mime types for the first Shared.MAX_DOCS_IN_INTENT
Steve McKayd8187962016-06-09 10:46:07 -0700192 final ContentResolver resolver = mContext.getContentResolver();
Steve McKayd8187962016-06-09 10:46:07 -0700193 final Set<String> clipTypes = new HashSet<>();
Garfield, Tanf46958b2016-06-17 15:32:28 -0700194 int docCount = 0;
195 for (String id : selection) {
196 assert(id != null);
197 Uri uri = uriBuilder.apply(id);
198 if (docCount++ < Shared.MAX_DOCS_IN_INTENT) {
199 DocumentInfo.addMimeTypes(resolver, uri, clipTypes);
200 clipItems.add(new ClipData.Item(uri));
201 }
Steve McKayd8187962016-06-09 10:46:07 -0700202
Garfield, Tanf46958b2016-06-17 15:32:28 -0700203 uris.add(uri);
204 }
205
206 // Prepare metadata
Steve McKayd8187962016-06-09 10:46:07 -0700207 PersistableBundle bundle = new PersistableBundle();
208 bundle.putInt(OP_TYPE_KEY, opType);
209 bundle.putInt(OP_JUMBO_SELECTION_SIZE, selection.size());
210
Garfield, Tanf46958b2016-06-17 15:32:28 -0700211 // Creates a clip tag
212 long tag = mClipStorage.createTag();
213 bundle.putLong(OP_JUMBO_SELECTION_TAG, tag);
Steve McKayd8187962016-06-09 10:46:07 -0700214
215 ClipDescription description = new ClipDescription(
216 "", // Currently "label" is not displayed anywhere in the UI.
217 clipTypes.toArray(new String[0]));
218 description.setExtras(bundle);
219
Garfield, Tanf46958b2016-06-17 15:32:28 -0700220 // Persists clip items
221 new ClipStorage.PersistTask(mClipStorage, uris, tag).execute();
222
Steve McKayd8187962016-06-09 10:46:07 -0700223 return new ClipData(description, clipItems);
Vladislav Kaznacheev067d6fa2016-05-03 15:37:21 -0700224 }
225
Ben Linb1404b72016-04-18 14:35:28 -0700226 /**
227 * Puts {@code ClipData} in a primary clipboard, describing a copy operation
228 */
Steve McKayd8187962016-06-09 10:46:07 -0700229 public void clipDocumentsForCopy(Function<String, Uri> uriBuilder, Selection selection) {
230 ClipData data =
231 getClipDataForDocuments(uriBuilder, selection, FileOperationService.OPERATION_COPY);
Ben Linb1404b72016-04-18 14:35:28 -0700232 assert(data != null);
233
Garfield, Tanf46958b2016-06-17 15:32:28 -0700234 setPrimaryClip(data);
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700235 }
Ben Linb1404b72016-04-18 14:35:28 -0700236
237 /**
238 * Puts {@Code ClipData} in a primary clipboard, describing a cut operation
239 */
Steve McKayd8187962016-06-09 10:46:07 -0700240 public void clipDocumentsForCut(
241 Function<String, Uri> uriBuilder, Selection selection, DocumentInfo parent) {
242 assert(!selection.isEmpty());
243 assert(parent.derivedUri != null);
Ben Linb1404b72016-04-18 14:35:28 -0700244
Steve McKayd8187962016-06-09 10:46:07 -0700245 ClipData data = getClipDataForDocuments(uriBuilder, selection,
246 FileOperationService.OPERATION_MOVE);
Ben Linb1404b72016-04-18 14:35:28 -0700247 assert(data != null);
248
249 PersistableBundle bundle = data.getDescription().getExtras();
Steve McKayd8187962016-06-09 10:46:07 -0700250 bundle.putString(SRC_PARENT_KEY, parent.derivedUri.toString());
Ben Linb1404b72016-04-18 14:35:28 -0700251
Garfield, Tanf46958b2016-06-17 15:32:28 -0700252 setPrimaryClip(data);
253 }
254
255 private void setPrimaryClip(ClipData data) {
256 deleteLastPrimaryClip();
257
258 long tag = getTag(data);
259 setLastUnusedPrimaryClipTag(tag);
260
Ben Linb1404b72016-04-18 14:35:28 -0700261 mClipboard.setPrimaryClip(data);
262 }
263
Garfield, Tanf46958b2016-06-17 15:32:28 -0700264 /**
265 * Sets this primary tag to both class variable and shared preference.
266 */
267 private void setLastUnusedPrimaryClipTag(long tag) {
268 mLastUnusedPrimaryClipTag = tag;
269 mPref.edit().putLong(LAST_PRIMARY_CLIP_TAG, tag).commit();
270 }
271
272 /**
273 * This is a good chance for us to remove previous clip file for cut/copy because we know a new
274 * primary clip is set.
275 */
276 @Override
277 public void onPrimaryClipChanged() {
278 deleteLastUnusedPrimaryClip();
279 }
280
281 private void deleteLastUnusedPrimaryClip() {
282 ClipData primary = mClipboard.getPrimaryClip();
283 long primaryTag = getTag(primary);
284
285 // onPrimaryClipChanged is also called after we call setPrimaryClip(), so make sure we don't
286 // delete the clip file we just created.
287 if (mLastUnusedPrimaryClipTag != primaryTag) {
288 deleteLastPrimaryClip();
Ben Linb1404b72016-04-18 14:35:28 -0700289 }
Garfield, Tanf46958b2016-06-17 15:32:28 -0700290 }
291
292 private void deleteLastPrimaryClip() {
293 deleteClip(mLastUnusedPrimaryClipTag);
294 setLastUnusedPrimaryClipTag(ClipStorage.NO_SELECTION_TAG);
295 }
296
297 /**
298 * Deletes the last seen drag clip file.
299 */
300 public void deleteDragClip() {
301 deleteClip(mLastDragClipTag);
302 mLastDragClipTag = ClipStorage.NO_SELECTION_TAG;
303 }
304
305 private void deleteClip(long tag) {
306 try {
307 mClipStorage.delete(tag);
308 } catch (IOException e) {
309 Log.w(TAG, "Error deleting clip file with tag: " + tag, e);
310 }
Ben Linb1404b72016-04-18 14:35:28 -0700311 }
312
Garfield, Tan6d0b46e2016-05-12 14:12:18 -0700313 /**
314 * Copies documents from clipboard. It's the same as {@link #copyFromClipData} with clipData
315 * returned from {@link ClipboardManager#getPrimaryClip()}.
316 *
317 * @param destination destination document.
318 * @param docStack the document stack to the destination folder,
319 * @param callback callback to notify when operation finishes.
320 */
Steve McKayd8187962016-06-09 10:46:07 -0700321 public void copyFromClipboard(
322 DocumentInfo destination,
323 DocumentStack docStack,
Garfield, Tan6d0b46e2016-05-12 14:12:18 -0700324 FileOperations.Callback callback) {
Steve McKayd8187962016-06-09 10:46:07 -0700325
Garfield, Tanf46958b2016-06-17 15:32:28 -0700326 // The primary clip has been claimed by a file operation. It's now the operation's duty
327 // to make sure the clip file is deleted after use.
328 setLastUnusedPrimaryClipTag(ClipStorage.NO_SELECTION_TAG);
329
Garfield, Tan6d0b46e2016-05-12 14:12:18 -0700330 copyFromClipData(destination, docStack, mClipboard.getPrimaryClip(), callback);
331 }
332
333 /**
334 * Copies documents from given clip data.
335 *
336 * @param destination destination document
337 * @param docStack the document stack to the destination folder
338 * @param clipData the clipData to copy from, or null to copy from clipboard
339 * @param callback callback to notify when operation finishes
340 */
Steve McKayd8187962016-06-09 10:46:07 -0700341 public void copyFromClipData(
342 final DocumentInfo destination,
343 DocumentStack docStack,
344 final @Nullable ClipData clipData,
345 final FileOperations.Callback callback) {
346
Garfield, Tan6d0b46e2016-05-12 14:12:18 -0700347 if (clipData == null) {
348 Log.i(TAG, "Received null clipData. Ignoring.");
349 return;
350 }
351
Garfield, Tanf46958b2016-06-17 15:32:28 -0700352 ClipDetails details = ClipDetails.createClipDetails(clipData);
Garfield, Tan6d0b46e2016-05-12 14:12:18 -0700353
Garfield, Tanf46958b2016-06-17 15:32:28 -0700354 if (!canCopy(destination)) {
355 callback.onOperationResult(
356 FileOperations.Callback.STATUS_REJECTED, details.getOpType(), 0);
Garfield, Tan6d0b46e2016-05-12 14:12:18 -0700357 return;
358 }
359
Garfield, Tanf46958b2016-06-17 15:32:28 -0700360 if (details.getItemCount() == 0) {
361 callback.onOperationResult(
362 FileOperations.Callback.STATUS_ACCEPTED, details.getOpType(), 0);
Garfield, Tan6d0b46e2016-05-12 14:12:18 -0700363 return;
364 }
365
366 DocumentStack dstStack = new DocumentStack();
367 dstStack.push(destination);
368 dstStack.addAll(docStack);
Garfield, Tanf46958b2016-06-17 15:32:28 -0700369
370 // Pass root here so that we can perform "download" root check when
371 dstStack.root = docStack.root;
372
373 FileOperations.start(mContext, details, dstStack, callback);
Garfield, Tan6d0b46e2016-05-12 14:12:18 -0700374 }
375
376 /**
377 * Returns true if the list of files can be copied to destination. Note that this
378 * is a policy check only. Currently the method does not attempt to verify
379 * available space or any other environmental aspects possibly resulting in
380 * failure to copy.
381 *
382 * @return true if the list of files can be copied to destination.
383 */
Garfield, Tanf46958b2016-06-17 15:32:28 -0700384 private static boolean canCopy(DocumentInfo dest) {
Garfield, Tan6d0b46e2016-05-12 14:12:18 -0700385 if (dest == null || !dest.isDirectory() || !dest.isCreateSupported()) {
386 return false;
387 }
388
Garfield, Tan6d0b46e2016-05-12 14:12:18 -0700389 return true;
390 }
391
Garfield, Tanf46958b2016-06-17 15:32:28 -0700392 /**
393 * Obtains tag from {@link ClipData}. Returns {@link ClipStorage#NO_SELECTION_TAG}
394 * if it's not a jumbo clip.
395 */
396 private static long getTag(@Nullable ClipData data) {
397 if (data == null) {
398 return ClipStorage.NO_SELECTION_TAG;
Ben Linb1404b72016-04-18 14:35:28 -0700399 }
Garfield, Tanf46958b2016-06-17 15:32:28 -0700400
401 ClipDescription description = data.getDescription();
402 BaseBundle bundle = description.getExtras();
403 return bundle.getLong(OP_JUMBO_SELECTION_TAG, ClipStorage.NO_SELECTION_TAG);
Ben Linb1404b72016-04-18 14:35:28 -0700404 }
Garfield, Tanf46958b2016-06-17 15:32:28 -0700405
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700406}