• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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 
17 package com.android.documentsui.clipping;
18 
19 import android.content.ClipData;
20 import android.content.ClipDescription;
21 import android.content.ClipboardManager;
22 import android.content.ContentResolver;
23 import android.content.Context;
24 import android.net.Uri;
25 import android.os.PersistableBundle;
26 import android.provider.DocumentsContract;
27 import android.support.annotation.Nullable;
28 import android.util.Log;
29 
30 import com.android.documentsui.base.DocumentInfo;
31 import com.android.documentsui.base.DocumentStack;
32 import com.android.documentsui.base.Features;
33 import com.android.documentsui.base.RootInfo;
34 import com.android.documentsui.base.Shared;
35 import com.android.documentsui.selection.Selection;
36 import com.android.documentsui.services.FileOperation;
37 import com.android.documentsui.services.FileOperationService;
38 import com.android.documentsui.services.FileOperationService.OpType;
39 import com.android.documentsui.services.FileOperations;
40 
41 import java.io.IOException;
42 import java.util.ArrayList;
43 import java.util.HashSet;
44 import java.util.List;
45 import java.util.Set;
46 import java.util.function.Function;
47 
48 /**
49  * ClipboardManager wrapper class providing higher level logical
50  * support for dealing with Documents.
51  */
52 final class RuntimeDocumentClipper implements DocumentClipper {
53 
54     private static final String TAG = "DocumentClipper";
55     private static final String SRC_PARENT_KEY = "srcParent";
56     private static final String OP_TYPE_KEY = "opType";
57 
58     private final Context mContext;
59     private final ClipStore mClipStore;
60     private final ClipboardManager mClipboard;
61 
RuntimeDocumentClipper(Context context, ClipStore clipStore)62     RuntimeDocumentClipper(Context context, ClipStore clipStore) {
63         mContext = context;
64         mClipStore = clipStore;
65         mClipboard = context.getSystemService(ClipboardManager.class);
66     }
67 
68     @Override
hasItemsToPaste()69     public boolean hasItemsToPaste() {
70         if (mClipboard.hasPrimaryClip()) {
71             ClipData clipData = mClipboard.getPrimaryClip();
72 
73             int count = clipData.getItemCount();
74             if (count > 0) {
75                 for (int i = 0; i < count; ++i) {
76                     ClipData.Item item = clipData.getItemAt(i);
77                     Uri uri = item.getUri();
78                     if (isDocumentUri(uri)) {
79                         return true;
80                     }
81                 }
82             }
83         }
84         return false;
85     }
86 
isDocumentUri(@ullable Uri uri)87     private boolean isDocumentUri(@Nullable Uri uri) {
88         return uri != null && DocumentsContract.isDocumentUri(mContext, uri);
89     }
90 
91     @Override
getClipDataForDocuments( Function<String, Uri> uriBuilder, Selection selection, @OpType int opType)92     public ClipData getClipDataForDocuments(
93         Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
94 
95         assert(selection != null);
96 
97         if (selection.isEmpty()) {
98             Log.w(TAG, "Attempting to clip empty selection. Ignoring.");
99             return null;
100         }
101 
102         return (selection.size() > Shared.MAX_DOCS_IN_INTENT)
103                 ? createJumboClipData(uriBuilder, selection, opType)
104                 : createStandardClipData(uriBuilder, selection, opType);
105     }
106 
107     /**
108      * Returns ClipData representing the selection.
109      */
createStandardClipData( Function<String, Uri> uriBuilder, Selection selection, @OpType int opType)110     private ClipData createStandardClipData(
111             Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
112 
113         assert(!selection.isEmpty());
114         assert(selection.size() <= Shared.MAX_DOCS_IN_INTENT);
115 
116         final ContentResolver resolver = mContext.getContentResolver();
117         final ArrayList<ClipData.Item> clipItems = new ArrayList<>();
118         final Set<String> clipTypes = new HashSet<>();
119 
120         PersistableBundle bundle = new PersistableBundle();
121         bundle.putInt(OP_TYPE_KEY, opType);
122 
123         for (String id : selection) {
124             assert(id != null);
125             Uri uri = uriBuilder.apply(id);
126             DocumentInfo.addMimeTypes(resolver, uri, clipTypes);
127             clipItems.add(new ClipData.Item(uri));
128         }
129 
130         ClipDescription description = new ClipDescription(
131                 "", // Currently "label" is not displayed anywhere in the UI.
132                 clipTypes.toArray(new String[0]));
133         description.setExtras(bundle);
134 
135         return createClipData(description, clipItems);
136     }
137 
138     /**
139      * Returns ClipData representing the list of docs
140      */
createJumboClipData( Function<String, Uri> uriBuilder, Selection selection, @OpType int opType)141     private ClipData createJumboClipData(
142             Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
143 
144         assert(!selection.isEmpty());
145         assert(selection.size() > Shared.MAX_DOCS_IN_INTENT);
146 
147         final List<Uri> uris = new ArrayList<>(selection.size());
148 
149         final int capacity = Math.min(selection.size(), Shared.MAX_DOCS_IN_INTENT);
150         final ArrayList<ClipData.Item> clipItems = new ArrayList<>(capacity);
151 
152         // Set up mime types for the first Shared.MAX_DOCS_IN_INTENT
153         final ContentResolver resolver = mContext.getContentResolver();
154         final Set<String> clipTypes = new HashSet<>();
155         int docCount = 0;
156         for (String id : selection) {
157             assert(id != null);
158             Uri uri = uriBuilder.apply(id);
159             if (docCount++ < Shared.MAX_DOCS_IN_INTENT) {
160                 DocumentInfo.addMimeTypes(resolver, uri, clipTypes);
161                 clipItems.add(new ClipData.Item(uri));
162             }
163 
164             uris.add(uri);
165         }
166 
167         // Prepare metadata
168         PersistableBundle bundle = new PersistableBundle();
169         bundle.putInt(OP_TYPE_KEY, opType);
170         bundle.putInt(OP_JUMBO_SELECTION_SIZE, selection.size());
171 
172         // Persists clip items and gets the slot they were saved under.
173         int tag = mClipStore.persistUris(uris);
174         bundle.putInt(OP_JUMBO_SELECTION_TAG, tag);
175 
176         ClipDescription description = new ClipDescription(
177                 "", // Currently "label" is not displayed anywhere in the UI.
178                 clipTypes.toArray(new String[0]));
179         description.setExtras(bundle);
180 
181         return createClipData(description, clipItems);
182     }
183 
184     @Override
clipDocumentsForCopy(Function<String, Uri> uriBuilder, Selection selection)185     public void clipDocumentsForCopy(Function<String, Uri> uriBuilder, Selection selection) {
186         ClipData data =
187                 getClipDataForDocuments(uriBuilder, selection, FileOperationService.OPERATION_COPY);
188         assert(data != null);
189 
190         mClipboard.setPrimaryClip(data);
191     }
192 
193     @Override
clipDocumentsForCut( Function<String, Uri> uriBuilder, Selection selection, DocumentInfo parent)194     public void clipDocumentsForCut(
195             Function<String, Uri> uriBuilder, Selection selection, DocumentInfo parent) {
196         assert(!selection.isEmpty());
197         assert(parent.derivedUri != null);
198 
199         ClipData data = getClipDataForDocuments(uriBuilder, selection,
200                 FileOperationService.OPERATION_MOVE);
201         assert(data != null);
202 
203         PersistableBundle bundle = data.getDescription().getExtras();
204         bundle.putString(SRC_PARENT_KEY, parent.derivedUri.toString());
205 
206         mClipboard.setPrimaryClip(data);
207     }
208 
209 
210     @Override
copyFromClipboard( DocumentInfo destination, DocumentStack docStack, FileOperations.Callback callback)211     public void copyFromClipboard(
212             DocumentInfo destination,
213             DocumentStack docStack,
214             FileOperations.Callback callback) {
215 
216         copyFromClipData(destination, docStack, mClipboard.getPrimaryClip(), callback);
217     }
218 
219     @Override
copyFromClipboard( DocumentStack docStack, FileOperations.Callback callback)220     public void copyFromClipboard(
221             DocumentStack docStack,
222             FileOperations.Callback callback) {
223 
224         copyFromClipData(docStack, mClipboard.getPrimaryClip(), callback);
225     }
226 
227     @Override
copyFromClipData( final RootInfo root, final DocumentInfo destination, final @Nullable ClipData clipData, final FileOperations.Callback callback)228     public void copyFromClipData(
229             final RootInfo root,
230             final DocumentInfo destination,
231             final @Nullable ClipData clipData,
232             final FileOperations.Callback callback) {
233         DocumentStack dstStack = new DocumentStack(root, destination);
234         copyFromClipData(dstStack, clipData, callback);
235     }
236 
237     @Override
copyFromClipData( final DocumentInfo destination, final DocumentStack docStack, final @Nullable ClipData clipData, final FileOperations.Callback callback)238     public void copyFromClipData(
239             final DocumentInfo destination,
240             final DocumentStack docStack,
241             final @Nullable ClipData clipData,
242             final FileOperations.Callback callback) {
243 
244         DocumentStack dstStack = new DocumentStack(docStack, destination);
245         copyFromClipData(dstStack, clipData, callback);
246     }
247 
248     @Override
copyFromClipData( final DocumentStack dstStack, final @Nullable ClipData clipData, final FileOperations.Callback callback)249     public void copyFromClipData(
250             final DocumentStack dstStack,
251             final @Nullable ClipData clipData,
252             final FileOperations.Callback callback) {
253 
254         if (clipData == null) {
255             Log.i(TAG, "Received null clipData. Ignoring.");
256             return;
257         }
258 
259         PersistableBundle bundle = clipData.getDescription().getExtras();
260         @OpType int opType = getOpType(bundle);
261         try {
262             if (!canCopy(dstStack.peek())) {
263                 callback.onOperationResult(
264                         FileOperations.Callback.STATUS_REJECTED, getOpType(clipData), 0);
265                 return;
266             }
267 
268             UrisSupplier uris = UrisSupplier.create(clipData, mClipStore);
269             if (uris.getItemCount() == 0) {
270                 callback.onOperationResult(
271                         FileOperations.Callback.STATUS_ACCEPTED, opType, 0);
272                 return;
273             }
274 
275             String srcParentString = bundle.getString(SRC_PARENT_KEY);
276             Uri srcParent = srcParentString == null ? null : Uri.parse(srcParentString);
277 
278             FileOperation operation = new FileOperation.Builder()
279                     .withOpType(opType)
280                     .withSrcParent(srcParent)
281                     .withDestination(dstStack)
282                     .withSrcs(uris)
283                     .build();
284 
285             FileOperations.start(mContext, operation, callback);
286         } catch(IOException e) {
287             Log.e(TAG, "Cannot create uris supplier.", e);
288             callback.onOperationResult(FileOperations.Callback.STATUS_REJECTED, opType, 0);
289             return;
290         }
291     }
292 
293     /**
294      * Returns true if the list of files can be copied to destination. Note that this
295      * is a policy check only. Currently the method does not attempt to verify
296      * available space or any other environmental aspects possibly resulting in
297      * failure to copy.
298      *
299      * @return true if the list of files can be copied to destination.
300      */
canCopy(@ullable DocumentInfo dest)301     private static boolean canCopy(@Nullable DocumentInfo dest) {
302         return dest != null && dest.isDirectory() && dest.isCreateSupported();
303     }
304 
305     @Override
getOpType(ClipData data)306     public @OpType int getOpType(ClipData data) {
307         PersistableBundle bundle = data.getDescription().getExtras();
308         return getOpType(bundle);
309     }
310 
getOpType(PersistableBundle bundle)311     private @OpType int getOpType(PersistableBundle bundle) {
312         return bundle.getInt(OP_TYPE_KEY);
313     }
314 
createClipData( ClipDescription description, ArrayList<ClipData.Item> clipItems)315     private static ClipData createClipData(
316             ClipDescription description, ArrayList<ClipData.Item> clipItems) {
317 
318         // technically we want to check >= O, but we'd need to patch back the O version code :|
319         if (Features.OMC_RUNTIME) {
320             return new ClipData(description, clipItems);
321         }
322 
323         ClipData clip = new ClipData(description, clipItems.get(0));
324         for (int i = 1; i < clipItems.size(); i++) {
325             clip.addItem(clipItems.get(i));
326         }
327         return clip;
328     }
329 }
330