ClipStorage.java revision b7e5f6b55f5075e4752bd94586211fae9af2bae0
184769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay/* 284769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay * Copyright (C) 2016 The Android Open Source Project 384769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay * 484769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay * Licensed under the Apache License, Version 2.0 (the "License"); 584769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay * you may not use this file except in compliance with the License. 684769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay * You may obtain a copy of the License at 784769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay * 884769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay * http://www.apache.org/licenses/LICENSE-2.0 984769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay * 1084769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay * Unless required by applicable law or agreed to in writing, software 1184769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay * distributed under the License is distributed on an "AS IS" BASIS, 1284769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1384769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay * See the License for the specific language governing permissions and 1484769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay * limitations under the License. 1584769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay */ 1684769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay 1784769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKaypackage com.android.documentsui; 1884769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay 19b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tanimport android.content.SharedPreferences; 2084769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKayimport android.net.Uri; 21edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tanimport android.os.AsyncTask; 2284769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKayimport android.support.annotation.VisibleForTesting; 23b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tanimport android.system.ErrnoException; 24b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tanimport android.system.Os; 25edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tanimport android.util.Log; 2684769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay 2784769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKayimport java.io.Closeable; 2884769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKayimport java.io.File; 29edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tanimport java.io.FileInputStream; 3084769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKayimport java.io.FileOutputStream; 3184769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKayimport java.io.IOException; 32edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tanimport java.nio.channels.FileLock; 33b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tanimport java.util.HashMap; 34b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tanimport java.util.Map; 35edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tanimport java.util.Scanner; 36b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tanimport java.util.concurrent.TimeUnit; 3784769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay 3884769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay/** 3984769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay * Provides support for storing lists of documents identified by Uri. 4084769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay * 41b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * This class uses a ring buffer to recycle clip file slots, to mitigate the issue of clip file 42b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * deletions. 4384769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay */ 4484769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKaypublic final class ClipStorage { 4584769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay 46b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan public static final int NO_SELECTION_TAG = -1; 47b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 48b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan static final String PREF_NAME = "ClipStoragePref"; 49b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 50b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan @VisibleForTesting 51b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan static final int NUM_OF_SLOTS = 20; 52b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 53edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan private static final String TAG = "ClipStorage"; 54edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 55b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan private static final long STALENESS_THRESHOLD = TimeUnit.DAYS.toMillis(2); 56b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 57b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan private static final String NEXT_POS_TAG = "NextPosTag"; 58b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan private static final String PRIMARY_DATA_FILE_NAME = "primary"; 59b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 6084769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay private static final byte[] LINE_SEPARATOR = System.lineSeparator().getBytes(); 6184769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay 6284769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay private final File mOutDir; 63b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan private final SharedPreferences mPref; 64b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 65b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan private final File[] mSlots = new File[NUM_OF_SLOTS]; 66b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan private int mNextPos; 6784769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay 6884769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay /** 6984769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay * @param outDir see {@link #prepareStorage(File)}. 7084769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay */ 71b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan public ClipStorage(File outDir, SharedPreferences pref) { 7284769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay assert(outDir.isDirectory()); 7384769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay mOutDir = outDir; 74b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan mPref = pref; 75b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 76b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan mNextPos = mPref.getInt(NEXT_POS_TAG, 0); 7784769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay } 7884769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay 7984769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay /** 80b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * Tries to get the next available clip slot. It's guaranteed to return one. If none of 81b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * slots is available, it returns the next slot of the most recently returned slot by this 82b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * method. 8384769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay * 84b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * <p>This is not a perfect solution, but should be enough for most regular use. There are 85b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * several situations this method may not work: 86b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * <ul> 87b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * <li>Making {@link #NUM_OF_SLOTS} - 1 times of large drag and drop or moveTo/copyTo/delete 88b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * operations after cutting a primary clip, then the primary clip is overwritten.</li> 89b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * <li>Having more than {@link #NUM_OF_SLOTS} queued jumbo file operations, one or more clip 90b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * file may be overwritten.</li> 91b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * </ul> 9284769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay */ 93b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan public synchronized int claimStorageSlot() { 94b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan int curPos = mNextPos; 95b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan for (int i = 0; i < NUM_OF_SLOTS; ++i, curPos = (curPos + 1) % NUM_OF_SLOTS) { 96b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan createSlotFile(curPos); 97b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 98b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan if (!mSlots[curPos].exists()) { 99b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan break; 100b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan } 101b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 102b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan // No file or only primary file exists, we deem it available. 103b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan if (mSlots[curPos].list().length <= 1) { 104b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan break; 105b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan } 106b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan // This slot doesn't seem available, but still need to check if it's a legacy of 107b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan // service being killed or a service crash etc. If it's stale, it's available. 108b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan else if(checkStaleFiles(curPos)) { 109b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan break; 110b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan } 111b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan } 112b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 113b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan prepareSlot(curPos); 114b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 115b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan mNextPos = (curPos + 1) % NUM_OF_SLOTS; 116b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan mPref.edit().putInt(NEXT_POS_TAG, mNextPos).commit(); 117b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan return curPos; 118b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan } 119b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 120b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan private boolean checkStaleFiles(int pos) { 121b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan File slotData = toSlotDataFile(pos); 122b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 123b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan // No need to check if the file exists. File.lastModified() returns 0L if the file doesn't 124b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan // exist. 125b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan return slotData.lastModified() + STALENESS_THRESHOLD <= System.currentTimeMillis(); 126b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan } 127b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 128b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan private void prepareSlot(int pos) { 129b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan assert(mSlots[pos] != null); 130b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 131b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan Files.deleteRecursively(mSlots[pos]); 132b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan mSlots[pos].mkdir(); 133b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan assert(mSlots[pos].isDirectory()); 13484769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay } 13584769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay 13684769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay /** 137edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan * Returns a writer. Callers must close the writer when finished. 13884769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay */ 139b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan private Writer createWriter(int tag) throws IOException { 140b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan File file = toSlotDataFile(tag); 141edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan return new Writer(file); 14284769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay } 14384769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay 144b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan /** 145b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * Gets a {@link File} instance given a tag. 146b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * 147b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * This method creates a symbolic link in the slot folder to the data file as a reference 148b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * counting method. When someone is done using this symlink, it's responsible to delete it. 149b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * Therefore we can have a neat way to track how many things are still using this slot. 150b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan */ 151b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan public File getFile(int tag) throws IOException { 152b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan createSlotFile(tag); 153b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 154b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan File primary = toSlotDataFile(tag); 155b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 156b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan String linkFileName = Integer.toString(mSlots[tag].list().length); 157b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan File link = new File(mSlots[tag], linkFileName); 158b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 159b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan try { 160b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan Os.symlink(primary.getAbsolutePath(), link.getAbsolutePath()); 161b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan } catch (ErrnoException e) { 162b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan e.rethrowAsIOException(); 163b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan } 164b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan return link; 165b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan } 166b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 167b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan /** 168b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * Returns a Reader. Callers must close the reader when finished. 169b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan */ 170b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan public Reader createReader(File file) throws IOException { 171b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan assert(file.getParentFile().getParentFile().equals(mOutDir)); 172edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan return new Reader(file); 17384769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay } 17484769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay 175b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan private File toSlotDataFile(int pos) { 176b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan assert(mSlots[pos] != null); 177b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan return new File(mSlots[pos], PRIMARY_DATA_FILE_NAME); 17884769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay } 17984769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay 180b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan private void createSlotFile(int pos) { 181b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan if (mSlots[pos] == null) { 182b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan mSlots[pos] = new File(mOutDir, Integer.toString(pos)); 183b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan } 18484769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay } 18584769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay 186edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan /** 187edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan * Provides initialization of the clip data storage directory. 188edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan */ 189edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan static File prepareStorage(File cacheDir) { 190edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan File clipDir = getClipDir(cacheDir); 191edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan clipDir.mkdir(); 192edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 193edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan assert(clipDir.isDirectory()); 194edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan return clipDir; 195edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan } 196edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 197edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan private static File getClipDir(File cacheDir) { 198edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan return new File(cacheDir, "clippings"); 199edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan } 200edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 201edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan static final class Reader implements Iterable<Uri>, Closeable { 202edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 203b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan /** 204b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * FileLock can't be held multiple times in a single JVM, but it's possible to have multiple 205b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * readers reading the same clip file. Share the FileLock here so that it can be released 206b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * when it's not needed. 207b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan */ 208b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan private static final Map<String, FileLockEntry> sLocks = new HashMap<>(); 209b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 210b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan private final String mCanonicalPath; 211edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan private final Scanner mScanner; 212edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 213edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan private Reader(File file) throws IOException { 214edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan FileInputStream inStream = new FileInputStream(file); 215edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan mScanner = new Scanner(inStream); 216b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 217b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan mCanonicalPath = file.getCanonicalPath(); // Resolve symlink 218b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan synchronized (sLocks) { 219b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan if (sLocks.containsKey(mCanonicalPath)) { 220b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan // Read lock is already held by someone in this JVM, just increment the ref 221b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan // count. 222b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan sLocks.get(mCanonicalPath).mCount++; 223b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan } else { 224b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan // No map entry, need to lock the file so it won't pass this line until the 225b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan // corresponding writer is done writing. 226b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan FileLock lock = inStream.getChannel().lock(0L, Long.MAX_VALUE, true); 227b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan sLocks.put(mCanonicalPath, new FileLockEntry(1, lock, mScanner)); 228b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan } 229b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan } 230edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan } 231edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 232edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan @Override 233edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan public Iterator iterator() { 234edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan return new Iterator(mScanner); 235edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan } 236edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 237edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan @Override 238edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan public void close() throws IOException { 239b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan synchronized (sLocks) { 240b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan FileLockEntry ref = sLocks.get(mCanonicalPath); 241b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 242b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan assert(ref.mCount > 0); 243b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan if (--ref.mCount == 0) { 244b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan // If ref count is 0 now, then there is no one who needs to hold the read lock. 245b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan // Release the lock, and remove the entry. 246b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan ref.mLock.release(); 247b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan ref.mScanner.close(); 248b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan sLocks.remove(mCanonicalPath); 249b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan } 250edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 251b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan if (mScanner != ref.mScanner) { 252b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan mScanner.close(); 253b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan } 254edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan } 255edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan } 256edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan } 257edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 258edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan private static final class Iterator implements java.util.Iterator { 259edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan private final Scanner mScanner; 260edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 261edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan private Iterator(Scanner scanner) { 262edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan mScanner = scanner; 263edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan } 264edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 265edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan @Override 266edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan public boolean hasNext() { 267edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan return mScanner.hasNextLine(); 268edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan } 269edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 270edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan @Override 271edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan public Uri next() { 272edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan String line = mScanner.nextLine(); 273edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan return Uri.parse(line); 274edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan } 275edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan } 276edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 277b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan private static final class FileLockEntry { 278b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan private int mCount; 279b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan private FileLock mLock; 280b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan // We need to keep this scanner here because if the scanner is closed, the file lock is 281b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan // closed too. 282b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan private Scanner mScanner; 283b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 284b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan private FileLockEntry(int count, FileLock lock, Scanner scanner) { 285b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan mCount = count; 286b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan mLock = lock; 287b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan mScanner = scanner; 288b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan } 289b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan } 290b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 291edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan private static final class Writer implements Closeable { 29284769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay 29384769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay private final FileOutputStream mOut; 294edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan private final FileLock mLock; 295edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 296edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan private Writer(File file) throws IOException { 297b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan assert(!file.exists()); 298b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan 299edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan mOut = new FileOutputStream(file); 30084769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay 301edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan // Lock the file here so copy tasks would wait until everything is flushed to disk 302edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan // before start to run. 303edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan mLock = mOut.getChannel().lock(); 30484769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay } 30584769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay 30684769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay public void write(Uri uri) throws IOException { 30784769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay mOut.write(uri.toString().getBytes()); 30884769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay mOut.write(LINE_SEPARATOR); 30984769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay } 31084769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay 31184769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay @Override 31284769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay public void close() throws IOException { 313edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan if (mLock != null) { 314edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan mLock.release(); 315edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan } 316edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 317edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan if (mOut != null) { 318edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan mOut.close(); 319edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan } 32084769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay } 32184769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay } 32284769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay 32384769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay /** 324edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan * An {@link AsyncTask} that persists doc uris in {@link ClipStorage}. 32584769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay */ 326edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan static final class PersistTask extends AsyncTask<Void, Void, Void> { 327edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 328edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan private final ClipStorage mClipStorage; 329edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan private final Iterable<Uri> mUris; 330b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan private final int mTag; 331edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 332b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan PersistTask(ClipStorage clipStorage, Iterable<Uri> uris, int tag) { 333edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan mClipStorage = clipStorage; 334edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan mUris = uris; 335edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan mTag = tag; 336edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan } 337edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 338edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan @Override 339edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan protected Void doInBackground(Void... params) { 340b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan try(Writer writer = mClipStorage.createWriter(mTag)){ 341edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan for (Uri uri: mUris) { 342edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan assert(uri != null); 343edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan writer.write(uri); 344edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan } 345edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan } catch (IOException e) { 346edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan Log.e(TAG, "Caught exception trying to write jumbo clip to disk.", e); 347edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan } 348edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 349edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan return null; 35084769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay } 35184769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay } 35284769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay} 353