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 static com.android.documentsui.clipping.DocumentClipper.OP_JUMBO_SELECTION_SIZE;
     20 import static com.android.documentsui.clipping.DocumentClipper.OP_JUMBO_SELECTION_TAG;
     21 
     22 import android.content.ClipData;
     23 import android.content.Context;
     24 import android.net.Uri;
     25 import android.os.Parcel;
     26 import android.os.Parcelable;
     27 import android.os.PersistableBundle;
     28 import android.support.annotation.VisibleForTesting;
     29 import android.util.Log;
     30 
     31 import com.android.documentsui.DocumentsApplication;
     32 import com.android.documentsui.base.Shared;
     33 import com.android.documentsui.selection.Selection;
     34 import com.android.documentsui.services.FileOperation;
     35 
     36 import java.io.File;
     37 import java.io.IOException;
     38 import java.util.ArrayList;
     39 import java.util.Collection;
     40 import java.util.List;
     41 import java.util.function.Function;
     42 
     43 /**
     44  * UrisSupplier provides doc uri list to {@link FileOperation}.
     45  *
     46  * <p>Under the hood it provides cross-process synchronization support such that its consumer doesn't
     47  * need to explicitly synchronize its access.
     48  */
     49 public abstract class UrisSupplier implements Parcelable {
     50 
     51     public abstract int getItemCount();
     52 
     53     /**
     54      * Gets doc list.
     55      *
     56      * @param context We need context to obtain {@link ClipStorage}. It can't be sent in a parcel.
     57      */
     58     public Iterable<Uri> getUris(Context context) throws IOException {
     59         return getUris(DocumentsApplication.getClipStore(context));
     60     }
     61 
     62     @VisibleForTesting
     63     abstract Iterable<Uri> getUris(ClipStore storage) throws IOException;
     64 
     65     public void dispose() {}
     66 
     67     @Override
     68     public int describeContents() {
     69         return 0;
     70     }
     71 
     72     public static UrisSupplier create(ClipData clipData, ClipStore storage) throws IOException {
     73         UrisSupplier uris;
     74         PersistableBundle bundle = clipData.getDescription().getExtras();
     75         if (bundle.containsKey(OP_JUMBO_SELECTION_TAG)) {
     76             uris = new JumboUrisSupplier(clipData, storage);
     77         } else {
     78             uris = new StandardUrisSupplier(clipData);
     79         }
     80 
     81         return uris;
     82     }
     83 
     84     public static UrisSupplier create(
     85             Selection selection, Function<String, Uri> uriBuilder, ClipStore storage)
     86             throws IOException {
     87 
     88         List<Uri> uris = new ArrayList<>(selection.size());
     89         for (String id : selection) {
     90             uris.add(uriBuilder.apply(id));
     91         }
     92 
     93         return create(uris, storage);
     94     }
     95 
     96     @VisibleForTesting
     97     static UrisSupplier create(List<Uri> uris, ClipStore storage) throws IOException {
     98         UrisSupplier urisSupplier = (uris.size() > Shared.MAX_DOCS_IN_INTENT)
     99                 ? new JumboUrisSupplier(uris, storage)
    100                 : new StandardUrisSupplier(uris);
    101 
    102         return urisSupplier;
    103     }
    104 
    105     private static class JumboUrisSupplier extends UrisSupplier {
    106         private static final String TAG = "JumboUrisSupplier";
    107 
    108         private final File mFile;
    109         private final int mSelectionSize;
    110 
    111         private final List<ClipStorageReader> mReaders = new ArrayList<>();
    112 
    113         private JumboUrisSupplier(ClipData clipData, ClipStore storage) throws IOException {
    114             PersistableBundle bundle = clipData.getDescription().getExtras();
    115             final int tag = bundle.getInt(OP_JUMBO_SELECTION_TAG, ClipStorage.NO_SELECTION_TAG);
    116             assert(tag != ClipStorage.NO_SELECTION_TAG);
    117             mFile = storage.getFile(tag);
    118             assert(mFile.exists());
    119 
    120             mSelectionSize = bundle.getInt(OP_JUMBO_SELECTION_SIZE);
    121             assert(mSelectionSize > Shared.MAX_DOCS_IN_INTENT);
    122         }
    123 
    124         private JumboUrisSupplier(Collection<Uri> uris, ClipStore clipStore) throws IOException {
    125             final int tag = clipStore.persistUris(uris);
    126 
    127             // There is a tiny race condition here. A job may starts to read before persist task
    128             // starts to write, but it has to beat an IPC and background task schedule, which is
    129             // pretty rare. Creating a symlink doesn't need that file to exist, but we can't assert
    130             // on its existence.
    131             mFile = clipStore.getFile(tag);
    132             mSelectionSize = uris.size();
    133         }
    134 
    135         @Override
    136         public int getItemCount() {
    137             return mSelectionSize;
    138         }
    139 
    140         @Override
    141         Iterable<Uri> getUris(ClipStore storage) throws IOException {
    142             ClipStorageReader reader = storage.createReader(mFile);
    143             synchronized (mReaders) {
    144                 mReaders.add(reader);
    145             }
    146 
    147             return reader;
    148         }
    149 
    150         @Override
    151         public void dispose() {
    152             synchronized (mReaders) {
    153                 for (ClipStorageReader reader : mReaders) {
    154                     try {
    155                         reader.close();
    156                     } catch (IOException e) {
    157                         Log.w(TAG, "Failed to close a reader.", e);
    158                     }
    159                 }
    160             }
    161 
    162             // mFile is a symlink to the actual data file. Delete the symlink here so that we know
    163             // there is one fewer referrer that needs the data file. The actual data file will be
    164             // cleaned up during file slot rotation. See ClipStorage for more details.
    165             mFile.delete();
    166         }
    167 
    168         @Override
    169         public String toString() {
    170             StringBuilder builder = new StringBuilder();
    171             builder.append("JumboUrisSupplier{");
    172             builder.append("file=").append(mFile.getAbsolutePath());
    173             builder.append(", selectionSize=").append(mSelectionSize);
    174             builder.append("}");
    175             return builder.toString();
    176         }
    177 
    178         @Override
    179         public void writeToParcel(Parcel dest, int flags) {
    180             dest.writeString(mFile.getAbsolutePath());
    181             dest.writeInt(mSelectionSize);
    182         }
    183 
    184         private JumboUrisSupplier(Parcel in) {
    185             mFile = new File(in.readString());
    186             mSelectionSize = in.readInt();
    187         }
    188 
    189         public static final Parcelable.Creator<JumboUrisSupplier> CREATOR =
    190                 new Parcelable.Creator<JumboUrisSupplier>() {
    191 
    192                     @Override
    193                     public JumboUrisSupplier createFromParcel(Parcel source) {
    194                         return new JumboUrisSupplier(source);
    195                     }
    196 
    197                     @Override
    198                     public JumboUrisSupplier[] newArray(int size) {
    199                         return new JumboUrisSupplier[size];
    200                     }
    201                 };
    202     }
    203 
    204     /**
    205      * This class and its constructor is visible for testing to create test doubles of
    206      * {@link UrisSupplier}.
    207      */
    208     @VisibleForTesting
    209     public static class StandardUrisSupplier extends UrisSupplier {
    210         private final List<Uri> mDocs;
    211 
    212         private StandardUrisSupplier(ClipData clipData) {
    213             mDocs = listDocs(clipData);
    214         }
    215 
    216         @VisibleForTesting
    217         public StandardUrisSupplier(List<Uri> docs) {
    218             mDocs = docs;
    219         }
    220 
    221         private List<Uri> listDocs(ClipData clipData) {
    222             ArrayList<Uri> docs = new ArrayList<>(clipData.getItemCount());
    223 
    224             for (int i = 0; i < clipData.getItemCount(); ++i) {
    225                 Uri uri = clipData.getItemAt(i).getUri();
    226                 assert(uri != null);
    227                 docs.add(uri);
    228             }
    229 
    230             return docs;
    231         }
    232 
    233         @Override
    234         public int getItemCount() {
    235             return mDocs.size();
    236         }
    237 
    238         @Override
    239         Iterable<Uri> getUris(ClipStore storage) {
    240             return mDocs;
    241         }
    242 
    243         @Override
    244         public String toString() {
    245             StringBuilder builder = new StringBuilder();
    246             builder.append("StandardUrisSupplier{");
    247             builder.append("docs=").append(mDocs.toString());
    248             builder.append("}");
    249             return builder.toString();
    250         }
    251 
    252         @Override
    253         public void writeToParcel(Parcel dest, int flags) {
    254             dest.writeTypedList(mDocs);
    255         }
    256 
    257         private StandardUrisSupplier(Parcel in) {
    258             mDocs = in.createTypedArrayList(Uri.CREATOR);
    259         }
    260 
    261         public static final Parcelable.Creator<StandardUrisSupplier> CREATOR =
    262                 new Parcelable.Creator<StandardUrisSupplier>() {
    263 
    264                     @Override
    265                     public StandardUrisSupplier createFromParcel(Parcel source) {
    266                         return new StandardUrisSupplier(source);
    267                     }
    268 
    269                     @Override
    270                     public StandardUrisSupplier[] newArray(int size) {
    271                         return new StandardUrisSupplier[size];
    272                     }
    273                 };
    274     }
    275 }
    276