/* * 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}. * *

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 getUris(Context context) throws IOException { return getUris(DocumentsApplication.getClipStore(context)); } @VisibleForTesting abstract Iterable 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 selection, Function uriBuilder, ClipStore storage) throws IOException { List uris = new ArrayList<>(selection.size()); for (String id : selection) { uris.add(uriBuilder.apply(id)); } return create(uris, storage); } @VisibleForTesting static UrisSupplier create(List 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 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 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 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 CREATOR = new Parcelable.Creator() { @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 mDocs; private StandardUrisSupplier(ClipData clipData) { mDocs = listDocs(clipData); } @VisibleForTesting public StandardUrisSupplier(List docs) { mDocs = docs; } private List listDocs(ClipData clipData) { ArrayList 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 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 CREATOR = new Parcelable.Creator() { @Override public StandardUrisSupplier createFromParcel(Parcel source) { return new StandardUrisSupplier(source); } @Override public StandardUrisSupplier[] newArray(int size) { return new StandardUrisSupplier[size]; } }; } }