Home | History | Annotate | Download | only in clipping
      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 android.content.SharedPreferences;
     20 import android.net.Uri;
     21 import android.os.AsyncTask;
     22 import android.support.annotation.VisibleForTesting;
     23 import android.system.ErrnoException;
     24 import android.system.Os;
     25 import android.util.Log;
     26 
     27 import com.android.documentsui.base.Files;
     28 
     29 import java.io.Closeable;
     30 import java.io.File;
     31 import java.io.FileOutputStream;
     32 import java.io.IOException;
     33 import java.nio.channels.FileLock;
     34 import java.util.concurrent.TimeUnit;
     35 
     36 /**
     37  * Provides support for storing lists of documents identified by Uri.
     38  *
     39  * This class uses a ring buffer to recycle clip file slots, to mitigate the issue of clip file
     40  * deletions. Below is the directory layout:
     41  * [cache dir]
     42  *      - [dir] 1
     43  *      - [dir] 2
     44  *      - ... to {@link #NUM_OF_SLOTS}
     45  * When a clip data is actively being used:
     46  * [cache dir]
     47  *      - [dir] 1
     48  *          - [file] primary
     49  *          - [symlink] 1 > primary # copying to location X
     50  *          - [symlink] 2 > primary # copying to location Y
     51  */
     52 public final class ClipStorage implements ClipStore {
     53 
     54     public static final int NO_SELECTION_TAG = -1;
     55 
     56     public static final String PREF_NAME = "ClipStoragePref";
     57 
     58     @VisibleForTesting
     59     static final int NUM_OF_SLOTS = 20;
     60 
     61     private static final String TAG = "ClipStorage";
     62 
     63     private static final long STALENESS_THRESHOLD = TimeUnit.DAYS.toMillis(2);
     64 
     65     private static final String NEXT_AVAIL_SLOT = "NextAvailableSlot";
     66     private static final String PRIMARY_DATA_FILE_NAME = "primary";
     67 
     68     private static final byte[] LINE_SEPARATOR = System.lineSeparator().getBytes();
     69 
     70     private final File mOutDir;
     71     private final SharedPreferences mPref;
     72 
     73     private final File[] mSlots = new File[NUM_OF_SLOTS];
     74     private int mNextSlot;
     75 
     76     /**
     77      * @param outDir see {@link #prepareStorage(File)}.
     78      */
     79     public ClipStorage(File outDir, SharedPreferences pref) {
     80         assert(outDir.isDirectory());
     81         mOutDir = outDir;
     82         mPref = pref;
     83 
     84         mNextSlot = mPref.getInt(NEXT_AVAIL_SLOT, 0);
     85     }
     86 
     87     /**
     88      * Tries to get the next available clip slot. It's guaranteed to return one. If none of
     89      * slots is available, it returns the next slot of the most recently returned slot by this
     90      * method.
     91      *
     92      * <p>This is not a perfect solution, but should be enough for most regular use. There are
     93      * several situations this method may not work:
     94      * <ul>
     95      *     <li>Making {@link #NUM_OF_SLOTS} - 1 times of large drag and drop or moveTo/copyTo/delete
     96      *     operations after cutting a primary clip, then the primary clip is overwritten.</li>
     97      *     <li>Having more than {@link #NUM_OF_SLOTS} queued jumbo file operations, one or more clip
     98      *     file may be overwritten.</li>
     99      * </ul>
    100      *
    101      * Implementations should take caution to serialize access.
    102      */
    103     @VisibleForTesting
    104     synchronized int claimStorageSlot() {
    105         int curSlot = mNextSlot;
    106         for (int i = 0; i < NUM_OF_SLOTS; ++i, curSlot = (curSlot + 1) % NUM_OF_SLOTS) {
    107             createSlotFileObject(curSlot);
    108 
    109             if (!mSlots[curSlot].exists()) {
    110                 break;
    111             }
    112 
    113             // No file or only primary file exists, we deem it available.
    114             if (mSlots[curSlot].list().length <= 1) {
    115                 break;
    116             }
    117             // This slot doesn't seem available, but still need to check if it's a legacy of
    118             // service being killed or a service crash etc. If it's stale, it's available.
    119             else if (checkStaleFiles(curSlot)) {
    120                 break;
    121             }
    122         }
    123 
    124         prepareSlot(curSlot);
    125 
    126         mNextSlot = (curSlot + 1) % NUM_OF_SLOTS;
    127         mPref.edit().putInt(NEXT_AVAIL_SLOT, mNextSlot).commit();
    128         return curSlot;
    129     }
    130 
    131     private boolean checkStaleFiles(int pos) {
    132         File slotData = toSlotDataFile(pos);
    133 
    134         // No need to check if the file exists. File.lastModified() returns 0L if the file doesn't
    135         // exist.
    136         return slotData.lastModified() + STALENESS_THRESHOLD <= System.currentTimeMillis();
    137     }
    138 
    139     private void prepareSlot(int pos) {
    140         assert(mSlots[pos] != null);
    141 
    142         Files.deleteRecursively(mSlots[pos]);
    143         mSlots[pos].mkdir();
    144         assert(mSlots[pos].isDirectory());
    145     }
    146 
    147     /**
    148      * Returns a writer. Callers must close the writer when finished.
    149      */
    150     private Writer createWriter(int slot) throws IOException {
    151         File file = toSlotDataFile(slot);
    152         return new Writer(file);
    153     }
    154 
    155     @Override
    156     public synchronized File getFile(int slot) throws IOException {
    157         createSlotFileObject(slot);
    158 
    159         File primary = toSlotDataFile(slot);
    160 
    161         String linkFileName = Integer.toString(mSlots[slot].list().length);
    162         File link = new File(mSlots[slot], linkFileName);
    163 
    164         try {
    165             Os.symlink(primary.getAbsolutePath(), link.getAbsolutePath());
    166         } catch (ErrnoException e) {
    167             e.rethrowAsIOException();
    168         }
    169         return link;
    170     }
    171 
    172     @Override
    173     public ClipStorageReader createReader(File file) throws IOException {
    174         assert(file.getParentFile().getParentFile().equals(mOutDir));
    175         return new ClipStorageReader(file);
    176     }
    177 
    178     private File toSlotDataFile(int pos) {
    179         assert(mSlots[pos] != null);
    180         return new File(mSlots[pos], PRIMARY_DATA_FILE_NAME);
    181     }
    182 
    183     private void createSlotFileObject(int pos) {
    184         if (mSlots[pos] == null) {
    185             mSlots[pos] = new File(mOutDir, Integer.toString(pos));
    186         }
    187     }
    188 
    189     /**
    190      * Provides initialization of the clip data storage directory.
    191      */
    192     public static File prepareStorage(File cacheDir) {
    193         File clipDir = getClipDir(cacheDir);
    194         clipDir.mkdir();
    195 
    196         assert(clipDir.isDirectory());
    197         return clipDir;
    198     }
    199 
    200     private static File getClipDir(File cacheDir) {
    201         return new File(cacheDir, "clippings");
    202     }
    203 
    204     public static final class Writer implements Closeable {
    205 
    206         private final FileOutputStream mOut;
    207         private final FileLock mLock;
    208 
    209         private Writer(File file) throws IOException {
    210             assert(!file.exists());
    211 
    212             mOut = new FileOutputStream(file);
    213 
    214             // Lock the file here so copy tasks would wait until everything is flushed to disk
    215             // before start to run.
    216             mLock = mOut.getChannel().lock();
    217         }
    218 
    219         public void write(Uri uri) throws IOException {
    220             mOut.write(uri.toString().getBytes());
    221             mOut.write(LINE_SEPARATOR);
    222         }
    223 
    224         @Override
    225         public void close() throws IOException {
    226             if (mLock != null) {
    227                 mLock.release();
    228             }
    229 
    230             if (mOut != null) {
    231                 mOut.close();
    232             }
    233         }
    234     }
    235 
    236     @Override
    237     public int persistUris(Iterable<Uri> uris) {
    238         int slot = claimStorageSlot();
    239         persistUris(uris, slot);
    240         return slot;
    241     }
    242 
    243     @VisibleForTesting
    244     void persistUris(Iterable<Uri> uris, int slot) {
    245         new PersistTask(this, uris, slot).execute();
    246     }
    247 
    248     /**
    249      * An {@link AsyncTask} that persists doc uris in {@link ClipStorage}.
    250      */
    251     private static final class PersistTask extends AsyncTask<Void, Void, Void> {
    252 
    253         private final ClipStorage mClipStore;
    254         private final Iterable<Uri> mUris;
    255         private final int mSlot;
    256 
    257         PersistTask(ClipStorage clipStore, Iterable<Uri> uris, int slot) {
    258             mClipStore = clipStore;
    259             mUris = uris;
    260             mSlot = slot;
    261         }
    262 
    263         @Override
    264         protected Void doInBackground(Void... params) {
    265             try(Writer writer = mClipStore.createWriter(mSlot)){
    266                 for (Uri uri: mUris) {
    267                     assert(uri != null);
    268                     writer.write(uri);
    269                 }
    270             } catch (IOException e) {
    271                 Log.e(TAG, "Caught exception trying to write jumbo clip to disk.", e);
    272             }
    273 
    274             return null;
    275         }
    276     }
    277 }
    278