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