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