blob: d6c0bd53b5cfe83a70014978d44e79cebc73c5cc [file] [log] [blame]
/*
* 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;
}
}