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