| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.documentsui.clipping; |
| |
| import android.content.ClipData; |
| import android.content.ClipDescription; |
| import android.content.ClipboardManager; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.net.Uri; |
| import android.os.PersistableBundle; |
| import android.provider.DocumentsContract; |
| import android.util.Log; |
| |
| import androidx.annotation.Nullable; |
| import androidx.recyclerview.selection.Selection; |
| |
| import com.android.documentsui.base.DocumentInfo; |
| import com.android.documentsui.base.DocumentStack; |
| import com.android.documentsui.base.Features; |
| import com.android.documentsui.base.Shared; |
| import com.android.documentsui.services.FileOperation; |
| import com.android.documentsui.services.FileOperationService; |
| import com.android.documentsui.services.FileOperationService.OpType; |
| import com.android.documentsui.services.FileOperations; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.function.Function; |
| |
| /** |
| * ClipboardManager wrapper class providing higher level logical |
| * support for dealing with Documents. |
| */ |
| final class RuntimeDocumentClipper implements DocumentClipper { |
| |
| private static final String TAG = "DocumentClipper"; |
| private static final String SRC_PARENT_KEY = "clipper:srcParent"; |
| private static final String OP_TYPE_KEY = "clipper:opType"; |
| |
| private final Context mContext; |
| private final ClipStore mClipStore; |
| private final ClipboardManager mClipboard; |
| |
| RuntimeDocumentClipper(Context context, ClipStore clipStore) { |
| mContext = context; |
| mClipStore = clipStore; |
| mClipboard = context.getSystemService(ClipboardManager.class); |
| } |
| |
| @Override |
| public boolean hasItemsToPaste() { |
| if (mClipboard.hasPrimaryClip()) { |
| ClipData clipData = mClipboard.getPrimaryClip(); |
| |
| int count = clipData.getItemCount(); |
| if (count > 0) { |
| for (int i = 0; i < count; ++i) { |
| ClipData.Item item = clipData.getItemAt(i); |
| Uri uri = item.getUri(); |
| if (isDocumentUri(uri)) { |
| return true; |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| private boolean isDocumentUri(@Nullable Uri uri) { |
| return uri != null && DocumentsContract.isDocumentUri(mContext, uri); |
| } |
| |
| @Override |
| public ClipData getClipDataForDocuments( |
| Function<String, Uri> uriBuilder, Selection<String> selection, @OpType int opType) { |
| |
| assert(selection != null); |
| |
| if (selection.isEmpty()) { |
| Log.w(TAG, "Attempting to clip empty selection. Ignoring."); |
| return null; |
| } |
| |
| final List<Uri> uris = new ArrayList<>(selection.size()); |
| for (String id : selection) { |
| uris.add(uriBuilder.apply(id)); |
| } |
| return getClipDataForDocuments(uris, opType); |
| } |
| |
| @Override |
| public ClipData getClipDataForDocuments( |
| List<Uri> uris, @OpType int opType, DocumentInfo parent) { |
| ClipData clipData = getClipDataForDocuments(uris, opType); |
| clipData.getDescription().getExtras().putString( |
| SRC_PARENT_KEY, parent.derivedUri.toString()); |
| return clipData; |
| } |
| |
| @Override |
| public ClipData getClipDataForDocuments(List<Uri> uris, @OpType int opType) { |
| return (uris.size() > Shared.MAX_DOCS_IN_INTENT) |
| ? createJumboClipData(uris, opType) |
| : createStandardClipData(uris, opType); |
| } |
| |
| /** |
| * Returns ClipData representing the selection. |
| */ |
| private ClipData createStandardClipData(List<Uri> uris, @OpType int opType) { |
| |
| assert(!uris.isEmpty()); |
| assert(uris.size() <= Shared.MAX_DOCS_IN_INTENT); |
| |
| final ContentResolver resolver = mContext.getContentResolver(); |
| final ArrayList<ClipData.Item> clipItems = new ArrayList<>(); |
| final Set<String> clipTypes = new HashSet<>(); |
| |
| PersistableBundle bundle = new PersistableBundle(); |
| bundle.putInt(OP_TYPE_KEY, opType); |
| |
| for (Uri uri : uris) { |
| DocumentInfo.addMimeTypes(resolver, uri, clipTypes); |
| clipItems.add(new ClipData.Item(uri)); |
| } |
| |
| ClipDescription description = new ClipDescription( |
| "", // Currently "label" is not displayed anywhere in the UI. |
| clipTypes.toArray(new String[0])); |
| description.setExtras(bundle); |
| |
| return createClipData(description, clipItems); |
| } |
| |
| /** |
| * Returns ClipData representing the list of docs |
| */ |
| private ClipData createJumboClipData(List<Uri> uris, @OpType int opType) { |
| |
| assert(!uris.isEmpty()); |
| assert(uris.size() > Shared.MAX_DOCS_IN_INTENT); |
| |
| final int capacity = Math.min(uris.size(), Shared.MAX_DOCS_IN_INTENT); |
| final ArrayList<ClipData.Item> clipItems = new ArrayList<>(capacity); |
| |
| // Set up mime types for the first Shared.MAX_DOCS_IN_INTENT |
| final ContentResolver resolver = mContext.getContentResolver(); |
| final Set<String> clipTypes = new HashSet<>(); |
| int docCount = 0; |
| for (Uri uri : uris) { |
| if (docCount++ < Shared.MAX_DOCS_IN_INTENT) { |
| DocumentInfo.addMimeTypes(resolver, uri, clipTypes); |
| clipItems.add(new ClipData.Item(uri)); |
| } |
| } |
| |
| // Prepare metadata |
| PersistableBundle bundle = new PersistableBundle(); |
| bundle.putInt(OP_TYPE_KEY, opType); |
| bundle.putInt(OP_JUMBO_SELECTION_SIZE, uris.size()); |
| |
| // Persists clip items and gets the slot they were saved under. |
| int tag = mClipStore.persistUris(uris); |
| bundle.putInt(OP_JUMBO_SELECTION_TAG, tag); |
| |
| ClipDescription description = new ClipDescription( |
| "", // Currently "label" is not displayed anywhere in the UI. |
| clipTypes.toArray(new String[0])); |
| description.setExtras(bundle); |
| |
| return createClipData(description, clipItems); |
| } |
| |
| @Override |
| public void clipDocumentsForCopy( |
| Function<String, Uri> uriBuilder, Selection<String> selection) { |
| ClipData data = |
| getClipDataForDocuments(uriBuilder, selection, FileOperationService.OPERATION_COPY); |
| assert(data != null); |
| |
| mClipboard.setPrimaryClip(data); |
| } |
| |
| @Override |
| public void clipDocumentsForCut( |
| Function<String, Uri> uriBuilder, Selection<String> selection, DocumentInfo parent) { |
| assert(!selection.isEmpty()); |
| assert(parent.derivedUri != null); |
| |
| ClipData data = getClipDataForDocuments(uriBuilder, selection, |
| FileOperationService.OPERATION_MOVE); |
| assert(data != null); |
| |
| PersistableBundle bundle = data.getDescription().getExtras(); |
| bundle.putString(SRC_PARENT_KEY, parent.derivedUri.toString()); |
| |
| mClipboard.setPrimaryClip(data); |
| } |
| |
| |
| @Override |
| public void copyFromClipboard( |
| DocumentInfo destination, |
| DocumentStack docStack, |
| FileOperations.Callback callback) { |
| |
| copyFromClipData(destination, docStack, mClipboard.getPrimaryClip(), callback); |
| } |
| |
| @Override |
| public void copyFromClipboard( |
| DocumentStack docStack, |
| FileOperations.Callback callback) { |
| |
| copyFromClipData(docStack, mClipboard.getPrimaryClip(), callback); |
| } |
| |
| @Override |
| public void copyFromClipData( |
| DocumentInfo destination, |
| DocumentStack docStack, |
| @Nullable ClipData clipData, |
| FileOperations.Callback callback) { |
| |
| DocumentStack dstStack = new DocumentStack(docStack, destination); |
| copyFromClipData(dstStack, clipData, callback); |
| } |
| |
| @Override |
| public void copyFromClipData( |
| DocumentStack dstStack, |
| ClipData clipData, |
| @OpType int opType, |
| FileOperations.Callback callback) { |
| |
| clipData.getDescription().getExtras().putInt(OP_TYPE_KEY, opType); |
| copyFromClipData(dstStack, clipData, callback); |
| } |
| |
| @Override |
| public void copyFromClipData( |
| DocumentStack dstStack, |
| @Nullable ClipData clipData, |
| FileOperations.Callback callback) { |
| |
| if (clipData == null) { |
| Log.i(TAG, "Received null clipData. Ignoring."); |
| return; |
| } |
| |
| PersistableBundle bundle = clipData.getDescription().getExtras(); |
| @OpType int opType = getOpType(bundle); |
| try { |
| if (!canCopy(dstStack.peek())) { |
| callback.onOperationResult( |
| FileOperations.Callback.STATUS_REJECTED, getOpType(clipData), 0); |
| return; |
| } |
| |
| UrisSupplier uris = UrisSupplier.create(clipData, mClipStore); |
| if (uris.getItemCount() == 0) { |
| callback.onOperationResult( |
| FileOperations.Callback.STATUS_ACCEPTED, opType, 0); |
| return; |
| } |
| |
| String srcParentString = bundle.getString(SRC_PARENT_KEY); |
| Uri srcParent = srcParentString == null ? null : Uri.parse(srcParentString); |
| |
| FileOperation operation = new FileOperation.Builder() |
| .withOpType(opType) |
| .withSrcParent(srcParent) |
| .withDestination(dstStack) |
| .withSrcs(uris) |
| .build(); |
| |
| FileOperations.start(mContext, operation, callback, FileOperations.createJobId()); |
| } catch (IOException e) { |
| Log.e(TAG, "Cannot create uris supplier.", e); |
| callback.onOperationResult(FileOperations.Callback.STATUS_REJECTED, opType, 0); |
| return; |
| } |
| } |
| |
| /** |
| * Returns true if the list of files can be copied to destination. Note that this |
| * is a policy check only. Currently the method does not attempt to verify |
| * available space or any other environmental aspects possibly resulting in |
| * failure to copy. |
| * |
| * @return true if the list of files can be copied to destination. |
| */ |
| private static boolean canCopy(@Nullable DocumentInfo dest) { |
| return dest != null && dest.isDirectory() && dest.isCreateSupported(); |
| } |
| |
| private @OpType int getOpType(ClipData data) { |
| PersistableBundle bundle = data.getDescription().getExtras(); |
| return getOpType(bundle); |
| } |
| |
| private @OpType int getOpType(PersistableBundle bundle) { |
| return bundle.getInt(OP_TYPE_KEY); |
| } |
| |
| private static ClipData createClipData( |
| ClipDescription description, ArrayList<ClipData.Item> clipItems) { |
| ClipData clip = new ClipData(description, clipItems.get(0)); |
| for (int i = 1; i < clipItems.size(); i++) { |
| clip.addItem(clipItems.get(i)); |
| } |
| return clip; |
| } |
| } |