Home | History | Annotate | Download | only in clipping
      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