| /* |
| * 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 android.content.SharedPreferences; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import androidx.annotation.VisibleForTesting; |
| import android.system.ErrnoException; |
| import android.system.Os; |
| import android.util.Log; |
| |
| import com.android.documentsui.base.Files; |
| |
| import java.io.Closeable; |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.nio.channels.FileLock; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * Provides support for storing lists of documents identified by Uri. |
| * |
| * This class uses a ring buffer to recycle clip file slots, to mitigate the issue of clip file |
| * deletions. Below is the directory layout: |
| * [cache dir] |
| * - [dir] 1 |
| * - [dir] 2 |
| * - ... to {@link #NUM_OF_SLOTS} |
| * When a clip data is actively being used: |
| * [cache dir] |
| * - [dir] 1 |
| * - [file] primary |
| * - [symlink] 1 > primary # copying to location X |
| * - [symlink] 2 > primary # copying to location Y |
| */ |
| public final class ClipStorage implements ClipStore { |
| |
| public static final int NO_SELECTION_TAG = -1; |
| |
| public static final String PREF_NAME = "ClipStoragePref"; |
| |
| @VisibleForTesting |
| static final int NUM_OF_SLOTS = 20; |
| |
| private static final String TAG = "ClipStorage"; |
| |
| private static final long STALENESS_THRESHOLD = TimeUnit.DAYS.toMillis(2); |
| |
| private static final String NEXT_AVAIL_SLOT = "NextAvailableSlot"; |
| private static final String PRIMARY_DATA_FILE_NAME = "primary"; |
| |
| private static final byte[] LINE_SEPARATOR = System.lineSeparator().getBytes(); |
| |
| private final File mOutDir; |
| private final SharedPreferences mPref; |
| |
| private final File[] mSlots = new File[NUM_OF_SLOTS]; |
| private int mNextSlot; |
| |
| /** |
| * @param outDir see {@link #prepareStorage(File)}. |
| */ |
| public ClipStorage(File outDir, SharedPreferences pref) { |
| assert(outDir.isDirectory()); |
| mOutDir = outDir; |
| mPref = pref; |
| |
| mNextSlot = mPref.getInt(NEXT_AVAIL_SLOT, 0); |
| } |
| |
| /** |
| * Tries to get the next available clip slot. It's guaranteed to return one. If none of |
| * slots is available, it returns the next slot of the most recently returned slot by this |
| * method. |
| * |
| * <p>This is not a perfect solution, but should be enough for most regular use. There are |
| * several situations this method may not work: |
| * <ul> |
| * <li>Making {@link #NUM_OF_SLOTS} - 1 times of large drag and drop or moveTo/copyTo/delete |
| * operations after cutting a primary clip, then the primary clip is overwritten.</li> |
| * <li>Having more than {@link #NUM_OF_SLOTS} queued jumbo file operations, one or more clip |
| * file may be overwritten.</li> |
| * </ul> |
| * |
| * Implementations should take caution to serialize access. |
| */ |
| @VisibleForTesting |
| synchronized int claimStorageSlot() { |
| int curSlot = mNextSlot; |
| for (int i = 0; i < NUM_OF_SLOTS; ++i, curSlot = (curSlot + 1) % NUM_OF_SLOTS) { |
| createSlotFileObject(curSlot); |
| |
| if (!mSlots[curSlot].exists()) { |
| break; |
| } |
| |
| // No file or only primary file exists, we deem it available. |
| if (mSlots[curSlot].list().length <= 1) { |
| break; |
| } |
| // This slot doesn't seem available, but still need to check if it's a legacy of |
| // service being killed or a service crash etc. If it's stale, it's available. |
| else if (checkStaleFiles(curSlot)) { |
| break; |
| } |
| } |
| |
| prepareSlot(curSlot); |
| |
| mNextSlot = (curSlot + 1) % NUM_OF_SLOTS; |
| mPref.edit().putInt(NEXT_AVAIL_SLOT, mNextSlot).commit(); |
| return curSlot; |
| } |
| |
| private boolean checkStaleFiles(int pos) { |
| File slotData = toSlotDataFile(pos); |
| |
| // No need to check if the file exists. File.lastModified() returns 0L if the file doesn't |
| // exist. |
| return slotData.lastModified() + STALENESS_THRESHOLD <= System.currentTimeMillis(); |
| } |
| |
| private void prepareSlot(int pos) { |
| assert(mSlots[pos] != null); |
| |
| Files.deleteRecursively(mSlots[pos]); |
| mSlots[pos].mkdir(); |
| assert(mSlots[pos].isDirectory()); |
| } |
| |
| /** |
| * Returns a writer. Callers must close the writer when finished. |
| */ |
| private Writer createWriter(int slot) throws IOException { |
| File file = toSlotDataFile(slot); |
| return new Writer(file); |
| } |
| |
| @Override |
| public synchronized File getFile(int slot) throws IOException { |
| createSlotFileObject(slot); |
| |
| File primary = toSlotDataFile(slot); |
| |
| String linkFileName = Integer.toString(mSlots[slot].list().length); |
| File link = new File(mSlots[slot], linkFileName); |
| |
| try { |
| Os.symlink(primary.getAbsolutePath(), link.getAbsolutePath()); |
| } catch (ErrnoException e) { |
| IOException newException = new IOException(e.getMessage()); |
| newException.initCause(e); |
| throw newException; |
| } |
| return link; |
| } |
| |
| @Override |
| public ClipStorageReader createReader(File file) throws IOException { |
| assert(file.getParentFile().getParentFile().equals(mOutDir)); |
| return new ClipStorageReader(file); |
| } |
| |
| private File toSlotDataFile(int pos) { |
| assert(mSlots[pos] != null); |
| return new File(mSlots[pos], PRIMARY_DATA_FILE_NAME); |
| } |
| |
| private void createSlotFileObject(int pos) { |
| if (mSlots[pos] == null) { |
| mSlots[pos] = new File(mOutDir, Integer.toString(pos)); |
| } |
| } |
| |
| /** |
| * Provides initialization of the clip data storage directory. |
| */ |
| public static File prepareStorage(File cacheDir) { |
| File clipDir = getClipDir(cacheDir); |
| clipDir.mkdir(); |
| |
| assert(clipDir.isDirectory()); |
| return clipDir; |
| } |
| |
| private static File getClipDir(File cacheDir) { |
| return new File(cacheDir, "clippings"); |
| } |
| |
| public static final class Writer implements Closeable { |
| |
| private final FileOutputStream mOut; |
| private final FileLock mLock; |
| |
| private Writer(File file) throws IOException { |
| assert(!file.exists()); |
| |
| mOut = new FileOutputStream(file); |
| |
| // Lock the file here so copy tasks would wait until everything is flushed to disk |
| // before start to run. |
| mLock = mOut.getChannel().lock(); |
| } |
| |
| public void write(Uri uri) throws IOException { |
| mOut.write(uri.toString().getBytes()); |
| mOut.write(LINE_SEPARATOR); |
| } |
| |
| @Override |
| public void close() throws IOException { |
| if (mLock != null) { |
| mLock.release(); |
| } |
| |
| if (mOut != null) { |
| mOut.close(); |
| } |
| } |
| } |
| |
| @Override |
| public int persistUris(Iterable<Uri> uris) { |
| int slot = claimStorageSlot(); |
| persistUris(uris, slot); |
| return slot; |
| } |
| |
| @VisibleForTesting |
| void persistUris(Iterable<Uri> uris, int slot) { |
| new PersistTask(this, uris, slot).execute(); |
| } |
| |
| /** |
| * An {@link AsyncTask} that persists doc uris in {@link ClipStorage}. |
| */ |
| private static final class PersistTask extends AsyncTask<Void, Void, Void> { |
| |
| private final ClipStorage mClipStore; |
| private final Iterable<Uri> mUris; |
| private final int mSlot; |
| |
| PersistTask(ClipStorage clipStore, Iterable<Uri> uris, int slot) { |
| mClipStore = clipStore; |
| mUris = uris; |
| mSlot = slot; |
| } |
| |
| @Override |
| protected Void doInBackground(Void... params) { |
| try(Writer writer = mClipStore.createWriter(mSlot)){ |
| for (Uri uri: mUris) { |
| assert(uri != null); |
| writer.write(uri); |
| } |
| } catch (IOException e) { |
| Log.e(TAG, "Caught exception trying to write jumbo clip to disk.", e); |
| } |
| |
| return null; |
| } |
| } |
| } |