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 62 RuntimeDocumentClipper(Context context, ClipStore clipStore) { 63 mContext = context; 64 mClipStore = clipStore; 65 mClipboard = context.getSystemService(ClipboardManager.class); 66 } 67 68 @Override 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 87 private boolean isDocumentUri(@Nullable Uri uri) { 88 return uri != null && DocumentsContract.isDocumentUri(mContext, uri); 89 } 90 91 @Override 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 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 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 */ 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 */ 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 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 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 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 228 public void copyFromClipboard( 229 DocumentStack docStack, 230 FileOperations.Callback callback) { 231 232 copyFromClipData(docStack, mClipboard.getPrimaryClip(), callback); 233 } 234 235 @Override 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 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 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 */ 310 private static boolean canCopy(@Nullable DocumentInfo dest) { 311 return dest != null && dest.isDirectory() && dest.isCreateSupported(); 312 } 313 314 private @OpType int getOpType(ClipData data) { 315 PersistableBundle bundle = data.getDescription().getExtras(); 316 return getOpType(bundle); 317 } 318 319 private @OpType int getOpType(PersistableBundle bundle) { 320 return bundle.getInt(OP_TYPE_KEY); 321 } 322 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