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