• 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 = "clipper:srcParent";
56     private static final String OP_TYPE_KEY = "clipper: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         final List<Uri> uris = new ArrayList<>(selection.size());
103         for (String id : selection) {
104             uris.add(uriBuilder.apply(id));
105         }
106         return getClipDataForDocuments(uris, opType);
107     }
108 
109     @Override
getClipDataForDocuments( List<Uri> uris, @OpType int opType, DocumentInfo parent)110     public ClipData getClipDataForDocuments(
111             List<Uri> uris, @OpType int opType, DocumentInfo parent) {
112         ClipData clipData = getClipDataForDocuments(uris, opType);
113         clipData.getDescription().getExtras().putString(
114                 SRC_PARENT_KEY, parent.derivedUri.toString());
115         return clipData;
116     }
117 
118     @Override
getClipDataForDocuments(List<Uri> uris, @OpType int opType)119     public ClipData getClipDataForDocuments(List<Uri> uris, @OpType int opType) {
120         return (uris.size() > Shared.MAX_DOCS_IN_INTENT)
121                 ? createJumboClipData(uris, opType)
122                 : createStandardClipData(uris, opType);
123     }
124 
125     /**
126      * Returns ClipData representing the selection.
127      */
createStandardClipData(List<Uri> uris, @OpType int opType)128     private ClipData createStandardClipData(List<Uri> uris, @OpType int opType) {
129 
130         assert(!uris.isEmpty());
131         assert(uris.size() <= Shared.MAX_DOCS_IN_INTENT);
132 
133         final ContentResolver resolver = mContext.getContentResolver();
134         final ArrayList<ClipData.Item> clipItems = new ArrayList<>();
135         final Set<String> clipTypes = new HashSet<>();
136 
137         PersistableBundle bundle = new PersistableBundle();
138         bundle.putInt(OP_TYPE_KEY, opType);
139 
140         for (Uri uri : uris) {
141             DocumentInfo.addMimeTypes(resolver, uri, clipTypes);
142             clipItems.add(new ClipData.Item(uri));
143         }
144 
145         ClipDescription description = new ClipDescription(
146                 "", // Currently "label" is not displayed anywhere in the UI.
147                 clipTypes.toArray(new String[0]));
148         description.setExtras(bundle);
149 
150         return createClipData(description, clipItems);
151     }
152 
153     /**
154      * Returns ClipData representing the list of docs
155      */
createJumboClipData(List<Uri> uris, @OpType int opType)156     private ClipData createJumboClipData(List<Uri> uris, @OpType int opType) {
157 
158         assert(!uris.isEmpty());
159         assert(uris.size() > Shared.MAX_DOCS_IN_INTENT);
160 
161         final int capacity = Math.min(uris.size(), Shared.MAX_DOCS_IN_INTENT);
162         final ArrayList<ClipData.Item> clipItems = new ArrayList<>(capacity);
163 
164         // Set up mime types for the first Shared.MAX_DOCS_IN_INTENT
165         final ContentResolver resolver = mContext.getContentResolver();
166         final Set<String> clipTypes = new HashSet<>();
167         int docCount = 0;
168         for (Uri uri : uris) {
169             if (docCount++ < Shared.MAX_DOCS_IN_INTENT) {
170                 DocumentInfo.addMimeTypes(resolver, uri, clipTypes);
171                 clipItems.add(new ClipData.Item(uri));
172             }
173         }
174 
175         // Prepare metadata
176         PersistableBundle bundle = new PersistableBundle();
177         bundle.putInt(OP_TYPE_KEY, opType);
178         bundle.putInt(OP_JUMBO_SELECTION_SIZE, uris.size());
179 
180         // Persists clip items and gets the slot they were saved under.
181         int tag = mClipStore.persistUris(uris);
182         bundle.putInt(OP_JUMBO_SELECTION_TAG, tag);
183 
184         ClipDescription description = new ClipDescription(
185                 "", // Currently "label" is not displayed anywhere in the UI.
186                 clipTypes.toArray(new String[0]));
187         description.setExtras(bundle);
188 
189         return createClipData(description, clipItems);
190     }
191 
192     @Override
clipDocumentsForCopy(Function<String, Uri> uriBuilder, Selection selection)193     public void clipDocumentsForCopy(Function<String, Uri> uriBuilder, Selection selection) {
194         ClipData data =
195                 getClipDataForDocuments(uriBuilder, selection, FileOperationService.OPERATION_COPY);
196         assert(data != null);
197 
198         mClipboard.setPrimaryClip(data);
199     }
200 
201     @Override
clipDocumentsForCut( Function<String, Uri> uriBuilder, Selection selection, DocumentInfo parent)202     public void clipDocumentsForCut(
203             Function<String, Uri> uriBuilder, Selection selection, DocumentInfo parent) {
204         assert(!selection.isEmpty());
205         assert(parent.derivedUri != null);
206 
207         ClipData data = getClipDataForDocuments(uriBuilder, selection,
208                 FileOperationService.OPERATION_MOVE);
209         assert(data != null);
210 
211         PersistableBundle bundle = data.getDescription().getExtras();
212         bundle.putString(SRC_PARENT_KEY, parent.derivedUri.toString());
213 
214         mClipboard.setPrimaryClip(data);
215     }
216 
217 
218     @Override
copyFromClipboard( DocumentInfo destination, DocumentStack docStack, FileOperations.Callback callback)219     public void copyFromClipboard(
220             DocumentInfo destination,
221             DocumentStack docStack,
222             FileOperations.Callback callback) {
223 
224         copyFromClipData(destination, docStack, mClipboard.getPrimaryClip(), callback);
225     }
226 
227     @Override
copyFromClipboard( DocumentStack docStack, FileOperations.Callback callback)228     public void copyFromClipboard(
229             DocumentStack docStack,
230             FileOperations.Callback callback) {
231 
232         copyFromClipData(docStack, mClipboard.getPrimaryClip(), callback);
233     }
234 
235     @Override
copyFromClipData( DocumentInfo destination, DocumentStack docStack, @Nullable ClipData clipData, FileOperations.Callback callback)236     public void copyFromClipData(
237             DocumentInfo destination,
238             DocumentStack docStack,
239             @Nullable ClipData clipData,
240             FileOperations.Callback callback) {
241 
242         DocumentStack dstStack = new DocumentStack(docStack, destination);
243         copyFromClipData(dstStack, clipData, callback);
244     }
245 
246     @Override
copyFromClipData( DocumentStack dstStack, ClipData clipData, @OpType int opType, FileOperations.Callback callback)247     public void copyFromClipData(
248             DocumentStack dstStack,
249             ClipData clipData,
250             @OpType int opType,
251             FileOperations.Callback callback) {
252 
253         clipData.getDescription().getExtras().putInt(OP_TYPE_KEY, opType);
254         copyFromClipData(dstStack, clipData, callback);
255     }
256 
257     @Override
copyFromClipData( DocumentStack dstStack, @Nullable ClipData clipData, FileOperations.Callback callback)258     public void copyFromClipData(
259             DocumentStack dstStack,
260             @Nullable ClipData clipData,
261             FileOperations.Callback callback) {
262 
263         if (clipData == null) {
264             Log.i(TAG, "Received null clipData. Ignoring.");
265             return;
266         }
267 
268         PersistableBundle bundle = clipData.getDescription().getExtras();
269         @OpType int opType = getOpType(bundle);
270         try {
271             if (!canCopy(dstStack.peek())) {
272                 callback.onOperationResult(
273                         FileOperations.Callback.STATUS_REJECTED, getOpType(clipData), 0);
274                 return;
275             }
276 
277             UrisSupplier uris = UrisSupplier.create(clipData, mClipStore);
278             if (uris.getItemCount() == 0) {
279                 callback.onOperationResult(
280                         FileOperations.Callback.STATUS_ACCEPTED, opType, 0);
281                 return;
282             }
283 
284             String srcParentString = bundle.getString(SRC_PARENT_KEY);
285             Uri srcParent = srcParentString == null ? null : Uri.parse(srcParentString);
286 
287             FileOperation operation = new FileOperation.Builder()
288                     .withOpType(opType)
289                     .withSrcParent(srcParent)
290                     .withDestination(dstStack)
291                     .withSrcs(uris)
292                     .build();
293 
294             FileOperations.start(mContext, operation, callback, FileOperations.createJobId());
295         } catch (IOException e) {
296             Log.e(TAG, "Cannot create uris supplier.", e);
297             callback.onOperationResult(FileOperations.Callback.STATUS_REJECTED, opType, 0);
298             return;
299         }
300     }
301 
302     /**
303      * Returns true if the list of files can be copied to destination. Note that this
304      * is a policy check only. Currently the method does not attempt to verify
305      * available space or any other environmental aspects possibly resulting in
306      * failure to copy.
307      *
308      * @return true if the list of files can be copied to destination.
309      */
canCopy(@ullable DocumentInfo dest)310     private static boolean canCopy(@Nullable DocumentInfo dest) {
311         return dest != null && dest.isDirectory() && dest.isCreateSupported();
312     }
313 
getOpType(ClipData data)314     private @OpType int getOpType(ClipData data) {
315         PersistableBundle bundle = data.getDescription().getExtras();
316         return getOpType(bundle);
317     }
318 
getOpType(PersistableBundle bundle)319     private @OpType int getOpType(PersistableBundle bundle) {
320         return bundle.getInt(OP_TYPE_KEY);
321     }
322 
createClipData( ClipDescription description, ArrayList<ClipData.Item> clipItems)323     private static ClipData createClipData(
324             ClipDescription description, ArrayList<ClipData.Item> clipItems) {
325 
326         // technically we want to check >= O, but we'd need to patch back the O version code :|
327         if (Features.OMC_RUNTIME) {
328             return new ClipData(description, clipItems);
329         }
330 
331         ClipData clip = new ClipData(description, clipItems.get(0));
332         for (int i = 1; i < clipItems.size(); i++) {
333             clip.addItem(clipItems.get(i));
334         }
335         return clip;
336     }
337 }
338