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 @VisibleForTesting create(List<Uri> uris, ClipStore storage)98 static UrisSupplier create(List<Uri> uris, ClipStore storage) throws IOException { 99 UrisSupplier urisSupplier = (uris.size() > Shared.MAX_DOCS_IN_INTENT) 100 ? new JumboUrisSupplier(uris, storage) 101 : new StandardUrisSupplier(uris); 102 103 return urisSupplier; 104 } 105 106 private static class JumboUrisSupplier extends UrisSupplier { 107 private static final String TAG = "JumboUrisSupplier"; 108 109 private final File mFile; 110 private final int mSelectionSize; 111 112 private final List<ClipStorageReader> mReaders = new ArrayList<>(); 113 JumboUrisSupplier(ClipData clipData, ClipStore storage)114 private JumboUrisSupplier(ClipData clipData, ClipStore storage) throws IOException { 115 PersistableBundle bundle = clipData.getDescription().getExtras(); 116 final int tag = bundle.getInt(OP_JUMBO_SELECTION_TAG, ClipStorage.NO_SELECTION_TAG); 117 assert(tag != ClipStorage.NO_SELECTION_TAG); 118 mFile = storage.getFile(tag); 119 assert(mFile.exists()); 120 121 mSelectionSize = bundle.getInt(OP_JUMBO_SELECTION_SIZE); 122 assert(mSelectionSize > Shared.MAX_DOCS_IN_INTENT); 123 } 124 JumboUrisSupplier(Collection<Uri> uris, ClipStore clipStore)125 private JumboUrisSupplier(Collection<Uri> uris, ClipStore clipStore) throws IOException { 126 final int tag = clipStore.persistUris(uris); 127 128 // There is a tiny race condition here. A job may starts to read before persist task 129 // starts to write, but it has to beat an IPC and background task schedule, which is 130 // pretty rare. Creating a symlink doesn't need that file to exist, but we can't assert 131 // on its existence. 132 mFile = clipStore.getFile(tag); 133 mSelectionSize = uris.size(); 134 } 135 136 @Override getItemCount()137 public int getItemCount() { 138 return mSelectionSize; 139 } 140 141 @Override getUris(ClipStore storage)142 Iterable<Uri> getUris(ClipStore storage) throws IOException { 143 ClipStorageReader reader = storage.createReader(mFile); 144 synchronized (mReaders) { 145 mReaders.add(reader); 146 } 147 148 return reader; 149 } 150 151 @Override dispose()152 public void dispose() { 153 synchronized (mReaders) { 154 for (ClipStorageReader reader : mReaders) { 155 try { 156 reader.close(); 157 } catch (IOException e) { 158 Log.w(TAG, "Failed to close a reader.", e); 159 } 160 } 161 } 162 163 // mFile is a symlink to the actual data file. Delete the symlink here so that we know 164 // there is one fewer referrer that needs the data file. The actual data file will be 165 // cleaned up during file slot rotation. See ClipStorage for more details. 166 mFile.delete(); 167 } 168 169 @Override toString()170 public String toString() { 171 StringBuilder builder = new StringBuilder(); 172 builder.append("JumboUrisSupplier{"); 173 builder.append("file=").append(mFile.getAbsolutePath()); 174 builder.append(", selectionSize=").append(mSelectionSize); 175 builder.append("}"); 176 return builder.toString(); 177 } 178 179 @Override writeToParcel(Parcel dest, int flags)180 public void writeToParcel(Parcel dest, int flags) { 181 dest.writeString(mFile.getAbsolutePath()); 182 dest.writeInt(mSelectionSize); 183 } 184 JumboUrisSupplier(Parcel in)185 private JumboUrisSupplier(Parcel in) { 186 mFile = new File(in.readString()); 187 mSelectionSize = in.readInt(); 188 } 189 190 public static final Parcelable.Creator<JumboUrisSupplier> CREATOR = 191 new Parcelable.Creator<JumboUrisSupplier>() { 192 193 @Override 194 public JumboUrisSupplier createFromParcel(Parcel source) { 195 return new JumboUrisSupplier(source); 196 } 197 198 @Override 199 public JumboUrisSupplier[] newArray(int size) { 200 return new JumboUrisSupplier[size]; 201 } 202 }; 203 } 204 205 /** 206 * This class and its constructor is visible for testing to create test doubles of 207 * {@link UrisSupplier}. 208 */ 209 @VisibleForTesting 210 public static class StandardUrisSupplier extends UrisSupplier { 211 private final List<Uri> mDocs; 212 StandardUrisSupplier(ClipData clipData)213 private StandardUrisSupplier(ClipData clipData) { 214 mDocs = listDocs(clipData); 215 } 216 217 @VisibleForTesting StandardUrisSupplier(List<Uri> docs)218 public StandardUrisSupplier(List<Uri> docs) { 219 mDocs = docs; 220 } 221 listDocs(ClipData clipData)222 private List<Uri> listDocs(ClipData clipData) { 223 ArrayList<Uri> docs = new ArrayList<>(clipData.getItemCount()); 224 225 for (int i = 0; i < clipData.getItemCount(); ++i) { 226 Uri uri = clipData.getItemAt(i).getUri(); 227 assert(uri != null); 228 docs.add(uri); 229 } 230 231 return docs; 232 } 233 234 @Override getItemCount()235 public int getItemCount() { 236 return mDocs.size(); 237 } 238 239 @Override getUris(ClipStore storage)240 Iterable<Uri> getUris(ClipStore storage) { 241 return mDocs; 242 } 243 244 @Override toString()245 public String toString() { 246 StringBuilder builder = new StringBuilder(); 247 builder.append("StandardUrisSupplier{"); 248 builder.append("docs=").append(mDocs.toString()); 249 builder.append("}"); 250 return builder.toString(); 251 } 252 253 @Override writeToParcel(Parcel dest, int flags)254 public void writeToParcel(Parcel dest, int flags) { 255 dest.writeTypedList(mDocs); 256 } 257 StandardUrisSupplier(Parcel in)258 private StandardUrisSupplier(Parcel in) { 259 mDocs = in.createTypedArrayList(Uri.CREATOR); 260 } 261 262 public static final Parcelable.Creator<StandardUrisSupplier> CREATOR = 263 new Parcelable.Creator<StandardUrisSupplier>() { 264 265 @Override 266 public StandardUrisSupplier createFromParcel(Parcel source) { 267 return new StandardUrisSupplier(source); 268 } 269 270 @Override 271 public StandardUrisSupplier[] newArray(int size) { 272 return new StandardUrisSupplier[size]; 273 } 274 }; 275 } 276 } 277