| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.documentsui.clipping; |
| |
| import static com.android.documentsui.clipping.DocumentClipper.OP_JUMBO_SELECTION_SIZE; |
| import static com.android.documentsui.clipping.DocumentClipper.OP_JUMBO_SELECTION_TAG; |
| |
| import android.content.ClipData; |
| import android.content.Context; |
| import android.net.Uri; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.os.PersistableBundle; |
| import android.util.Log; |
| |
| import androidx.annotation.VisibleForTesting; |
| import androidx.recyclerview.selection.Selection; |
| |
| import com.android.documentsui.DocumentsApplication; |
| import com.android.documentsui.base.Shared; |
| import com.android.documentsui.services.FileOperation; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.function.Function; |
| |
| /** |
| * UrisSupplier provides doc uri list to {@link FileOperation}. |
| * |
| * <p>Under the hood it provides cross-process synchronization support such that its consumer doesn't |
| * need to explicitly synchronize its access. |
| */ |
| public abstract class UrisSupplier implements Parcelable { |
| |
| public abstract int getItemCount(); |
| |
| /** |
| * Gets doc list. |
| * |
| * @param context We need context to obtain {@link ClipStorage}. It can't be sent in a parcel. |
| */ |
| public Iterable<Uri> getUris(Context context) throws IOException { |
| return getUris(DocumentsApplication.getClipStore(context)); |
| } |
| |
| @VisibleForTesting |
| abstract Iterable<Uri> getUris(ClipStore storage) throws IOException; |
| |
| public void dispose() {} |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| public static UrisSupplier create(ClipData clipData, ClipStore storage) throws IOException { |
| UrisSupplier uris; |
| PersistableBundle bundle = clipData.getDescription().getExtras(); |
| if (bundle.containsKey(OP_JUMBO_SELECTION_TAG)) { |
| uris = new JumboUrisSupplier(clipData, storage); |
| } else { |
| uris = new StandardUrisSupplier(clipData); |
| } |
| |
| return uris; |
| } |
| |
| public static UrisSupplier create( |
| Selection<String> selection, Function<String, Uri> uriBuilder, ClipStore storage) |
| throws IOException { |
| |
| List<Uri> uris = new ArrayList<>(selection.size()); |
| for (String id : selection) { |
| uris.add(uriBuilder.apply(id)); |
| } |
| |
| return create(uris, storage); |
| } |
| |
| /** |
| * Get a uri supplier. |
| * |
| * @param uris uris of the selection. |
| * @param storage the ClipStorage. |
| */ |
| public static UrisSupplier create(List<Uri> uris, ClipStore storage) throws IOException { |
| UrisSupplier urisSupplier = (uris.size() > Shared.MAX_DOCS_IN_INTENT) |
| ? new JumboUrisSupplier(uris, storage) |
| : new StandardUrisSupplier(uris); |
| |
| return urisSupplier; |
| } |
| |
| private static class JumboUrisSupplier extends UrisSupplier { |
| private static final String TAG = "JumboUrisSupplier"; |
| |
| private final File mFile; |
| private final int mSelectionSize; |
| |
| private final List<ClipStorageReader> mReaders = new ArrayList<>(); |
| |
| private JumboUrisSupplier(ClipData clipData, ClipStore storage) throws IOException { |
| PersistableBundle bundle = clipData.getDescription().getExtras(); |
| final int tag = bundle.getInt(OP_JUMBO_SELECTION_TAG, ClipStorage.NO_SELECTION_TAG); |
| assert(tag != ClipStorage.NO_SELECTION_TAG); |
| mFile = storage.getFile(tag); |
| assert(mFile.exists()); |
| |
| mSelectionSize = bundle.getInt(OP_JUMBO_SELECTION_SIZE); |
| assert(mSelectionSize > Shared.MAX_DOCS_IN_INTENT); |
| } |
| |
| private JumboUrisSupplier(Collection<Uri> uris, ClipStore clipStore) throws IOException { |
| final int tag = clipStore.persistUris(uris); |
| |
| // There is a tiny race condition here. A job may starts to read before persist task |
| // starts to write, but it has to beat an IPC and background task schedule, which is |
| // pretty rare. Creating a symlink doesn't need that file to exist, but we can't assert |
| // on its existence. |
| mFile = clipStore.getFile(tag); |
| mSelectionSize = uris.size(); |
| } |
| |
| @Override |
| public int getItemCount() { |
| return mSelectionSize; |
| } |
| |
| @Override |
| Iterable<Uri> getUris(ClipStore storage) throws IOException { |
| ClipStorageReader reader = storage.createReader(mFile); |
| synchronized (mReaders) { |
| mReaders.add(reader); |
| } |
| |
| return reader; |
| } |
| |
| @Override |
| public void dispose() { |
| synchronized (mReaders) { |
| for (ClipStorageReader reader : mReaders) { |
| try { |
| reader.close(); |
| } catch (IOException e) { |
| Log.w(TAG, "Failed to close a reader.", e); |
| } |
| } |
| } |
| |
| // mFile is a symlink to the actual data file. Delete the symlink here so that we know |
| // there is one fewer referrer that needs the data file. The actual data file will be |
| // cleaned up during file slot rotation. See ClipStorage for more details. |
| mFile.delete(); |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder builder = new StringBuilder(); |
| builder.append("JumboUrisSupplier{"); |
| builder.append("file=").append(mFile.getAbsolutePath()); |
| builder.append(", selectionSize=").append(mSelectionSize); |
| builder.append("}"); |
| return builder.toString(); |
| } |
| |
| @Override |
| public void writeToParcel(Parcel dest, int flags) { |
| dest.writeString(mFile.getAbsolutePath()); |
| dest.writeInt(mSelectionSize); |
| } |
| |
| private JumboUrisSupplier(Parcel in) { |
| mFile = new File(in.readString()); |
| mSelectionSize = in.readInt(); |
| } |
| |
| public static final Parcelable.Creator<JumboUrisSupplier> CREATOR = |
| new Parcelable.Creator<JumboUrisSupplier>() { |
| |
| @Override |
| public JumboUrisSupplier createFromParcel(Parcel source) { |
| return new JumboUrisSupplier(source); |
| } |
| |
| @Override |
| public JumboUrisSupplier[] newArray(int size) { |
| return new JumboUrisSupplier[size]; |
| } |
| }; |
| } |
| |
| /** |
| * This class and its constructor is visible for testing to create test doubles of |
| * {@link UrisSupplier}. |
| */ |
| @VisibleForTesting |
| public static class StandardUrisSupplier extends UrisSupplier { |
| private final List<Uri> mDocs; |
| |
| private StandardUrisSupplier(ClipData clipData) { |
| mDocs = listDocs(clipData); |
| } |
| |
| @VisibleForTesting |
| public StandardUrisSupplier(List<Uri> docs) { |
| mDocs = docs; |
| } |
| |
| private List<Uri> listDocs(ClipData clipData) { |
| ArrayList<Uri> docs = new ArrayList<>(clipData.getItemCount()); |
| |
| for (int i = 0; i < clipData.getItemCount(); ++i) { |
| Uri uri = clipData.getItemAt(i).getUri(); |
| assert(uri != null); |
| docs.add(uri); |
| } |
| |
| return docs; |
| } |
| |
| @Override |
| public int getItemCount() { |
| return mDocs.size(); |
| } |
| |
| @Override |
| Iterable<Uri> getUris(ClipStore storage) { |
| return mDocs; |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder builder = new StringBuilder(); |
| builder.append("StandardUrisSupplier{"); |
| builder.append("docs=").append(mDocs.toString()); |
| builder.append("}"); |
| return builder.toString(); |
| } |
| |
| @Override |
| public void writeToParcel(Parcel dest, int flags) { |
| dest.writeTypedList(mDocs); |
| } |
| |
| private StandardUrisSupplier(Parcel in) { |
| mDocs = in.createTypedArrayList(Uri.CREATOR); |
| } |
| |
| public static final Parcelable.Creator<StandardUrisSupplier> CREATOR = |
| new Parcelable.Creator<StandardUrisSupplier>() { |
| |
| @Override |
| public StandardUrisSupplier createFromParcel(Parcel source) { |
| return new StandardUrisSupplier(source); |
| } |
| |
| @Override |
| public StandardUrisSupplier[] newArray(int size) { |
| return new StandardUrisSupplier[size]; |
| } |
| }; |
| } |
| } |