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