• 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.util.Log;
28 
29 import androidx.annotation.Nullable;
30 import androidx.recyclerview.selection.Selection;
31 
32 import com.android.documentsui.base.DocumentInfo;
33 import com.android.documentsui.base.DocumentStack;
34 import com.android.documentsui.base.Features;
35 import com.android.documentsui.base.Shared;
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<String> selection, @OpType int opType)92     public ClipData getClipDataForDocuments(
93         Function<String, Uri> uriBuilder, Selection<String> 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<String> selection)193     public void clipDocumentsForCopy(
194             Function<String, Uri> uriBuilder, Selection<String> selection) {
195         ClipData data =
196                 getClipDataForDocuments(uriBuilder, selection, FileOperationService.OPERATION_COPY);
197         assert(data != null);
198 
199         mClipboard.setPrimaryClip(data);
200     }
201 
202     @Override
clipDocumentsForCut( Function<String, Uri> uriBuilder, Selection<String> selection, DocumentInfo parent)203     public void clipDocumentsForCut(
204             Function<String, Uri> uriBuilder, Selection<String> selection, DocumentInfo parent) {
205         assert(!selection.isEmpty());
206         assert(parent.derivedUri != null);
207 
208         ClipData data = getClipDataForDocuments(uriBuilder, selection,
209                 FileOperationService.OPERATION_MOVE);
210         assert(data != null);
211 
212         PersistableBundle bundle = data.getDescription().getExtras();
213         bundle.putString(SRC_PARENT_KEY, parent.derivedUri.toString());
214 
215         mClipboard.setPrimaryClip(data);
216     }
217 
218 
219     @Override
copyFromClipboard( DocumentInfo destination, DocumentStack docStack, FileOperations.Callback callback)220     public void copyFromClipboard(
221             DocumentInfo destination,
222             DocumentStack docStack,
223             FileOperations.Callback callback) {
224 
225         copyFromClipData(destination, docStack, mClipboard.getPrimaryClip(), callback);
226     }
227 
228     @Override
copyFromClipboard( DocumentStack docStack, FileOperations.Callback callback)229     public void copyFromClipboard(
230             DocumentStack docStack,
231             FileOperations.Callback callback) {
232 
233         copyFromClipData(docStack, mClipboard.getPrimaryClip(), callback);
234     }
235 
236     @Override
copyFromClipData( DocumentInfo destination, DocumentStack docStack, @Nullable ClipData clipData, FileOperations.Callback callback)237     public void copyFromClipData(
238             DocumentInfo destination,
239             DocumentStack docStack,
240             @Nullable ClipData clipData,
241             FileOperations.Callback callback) {
242 
243         DocumentStack dstStack = new DocumentStack(docStack, destination);
244         copyFromClipData(dstStack, clipData, callback);
245     }
246 
247     @Override
copyFromClipData( DocumentStack dstStack, ClipData clipData, @OpType int opType, FileOperations.Callback callback)248     public void copyFromClipData(
249             DocumentStack dstStack,
250             ClipData clipData,
251             @OpType int opType,
252             FileOperations.Callback callback) {
253 
254         clipData.getDescription().getExtras().putInt(OP_TYPE_KEY, opType);
255         copyFromClipData(dstStack, clipData, callback);
256     }
257 
258     @Override
copyFromClipData( DocumentStack dstStack, @Nullable ClipData clipData, FileOperations.Callback callback)259     public void copyFromClipData(
260             DocumentStack dstStack,
261             @Nullable ClipData clipData,
262             FileOperations.Callback callback) {
263 
264         if (clipData == null) {
265             Log.i(TAG, "Received null clipData. Ignoring.");
266             return;
267         }
268 
269         PersistableBundle bundle = clipData.getDescription().getExtras();
270         @OpType int opType = getOpType(bundle);
271         try {
272             if (!canCopy(dstStack.peek())) {
273                 callback.onOperationResult(
274                         FileOperations.Callback.STATUS_REJECTED, getOpType(clipData), 0);
275                 return;
276             }
277 
278             UrisSupplier uris = UrisSupplier.create(clipData, mClipStore);
279             if (uris.getItemCount() == 0) {
280                 callback.onOperationResult(
281                         FileOperations.Callback.STATUS_ACCEPTED, opType, 0);
282                 return;
283             }
284 
285             String srcParentString = bundle.getString(SRC_PARENT_KEY);
286             Uri srcParent = srcParentString == null ? null : Uri.parse(srcParentString);
287 
288             FileOperation operation = new FileOperation.Builder()
289                     .withOpType(opType)
290                     .withSrcParent(srcParent)
291                     .withDestination(dstStack)
292                     .withSrcs(uris)
293                     .build();
294 
295             FileOperations.start(mContext, operation, callback, FileOperations.createJobId());
296         } catch (IOException e) {
297             Log.e(TAG, "Cannot create uris supplier.", e);
298             callback.onOperationResult(FileOperations.Callback.STATUS_REJECTED, opType, 0);
299             return;
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      */
canCopy(@ullable DocumentInfo dest)311     private static boolean canCopy(@Nullable DocumentInfo dest) {
312         return dest != null && dest.isDirectory() && dest.isCreateSupported();
313     }
314 
getOpType(ClipData data)315     private @OpType int getOpType(ClipData data) {
316         PersistableBundle bundle = data.getDescription().getExtras();
317         return getOpType(bundle);
318     }
319 
getOpType(PersistableBundle bundle)320     private @OpType int getOpType(PersistableBundle bundle) {
321         return bundle.getInt(OP_TYPE_KEY);
322     }
323 
createClipData( ClipDescription description, ArrayList<ClipData.Item> clipItems)324     private static ClipData createClipData(
325             ClipDescription description, ArrayList<ClipData.Item> clipItems) {
326         ClipData clip = new ClipData(description, clipItems.get(0));
327         for (int i = 1; i < clipItems.size(); i++) {
328             clip.addItem(clipItems.get(i));
329         }
330         return clip;
331     }
332 }
333