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
179666ce691a4d45344460f17cec3577dbe075235aGarfield, Tanpackage com.android.documentsui.clipping;
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
27d9caa6ab53aa784acaf241c0ded3c4ae2d342bf8Steve McKayimport com.android.documentsui.base.Files;
289666ce691a4d45344460f17cec3577dbe075235aGarfield, Tan
2984769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKayimport java.io.Closeable;
3084769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKayimport java.io.File;
3184769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKayimport java.io.FileOutputStream;
3284769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKayimport java.io.IOException;
33edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tanimport java.nio.channels.FileLock;
34b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tanimport java.util.concurrent.TimeUnit;
3584769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay
3684769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay/**
3784769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay * Provides support for storing lists of documents identified by Uri.
3884769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay *
39b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan * This class uses a ring buffer to recycle clip file slots, to mitigate the issue of clip file
4064936a7f5027a8592d227a3d058fd97b4d134dc1Garfield, Tan * deletions. Below is the directory layout:
4164936a7f5027a8592d227a3d058fd97b4d134dc1Garfield, Tan * [cache dir]
4264936a7f5027a8592d227a3d058fd97b4d134dc1Garfield, Tan *      - [dir] 1
4364936a7f5027a8592d227a3d058fd97b4d134dc1Garfield, Tan *      - [dir] 2
4464936a7f5027a8592d227a3d058fd97b4d134dc1Garfield, Tan *      - ... to {@link #NUM_OF_SLOTS}
4564936a7f5027a8592d227a3d058fd97b4d134dc1Garfield, Tan * When a clip data is actively being used:
4664936a7f5027a8592d227a3d058fd97b4d134dc1Garfield, Tan * [cache dir]
4764936a7f5027a8592d227a3d058fd97b4d134dc1Garfield, Tan *      - [dir] 1
4864936a7f5027a8592d227a3d058fd97b4d134dc1Garfield, Tan *          - [file] primary
4964936a7f5027a8592d227a3d058fd97b4d134dc1Garfield, Tan *          - [symlink] 1 > primary # copying to location X
5064936a7f5027a8592d227a3d058fd97b4d134dc1Garfield, Tan *          - [symlink] 2 > primary # copying to location Y
5184769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay */
52c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKaypublic final class ClipStorage implements ClipStore {
5384769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay
54b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan    public static final int NO_SELECTION_TAG = -1;
55b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan
569666ce691a4d45344460f17cec3577dbe075235aGarfield, Tan    public static final String PREF_NAME = "ClipStoragePref";
57b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan
58b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan    @VisibleForTesting
59b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan    static final int NUM_OF_SLOTS = 20;
60b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan
61edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan    private static final String TAG = "ClipStorage";
62edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan
63b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan    private static final long STALENESS_THRESHOLD = TimeUnit.DAYS.toMillis(2);
64b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan
65c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay    private static final String NEXT_AVAIL_SLOT = "NextAvailableSlot";
66b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan    private static final String PRIMARY_DATA_FILE_NAME = "primary";
67b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan
6884769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay    private static final byte[] LINE_SEPARATOR = System.lineSeparator().getBytes();
6984769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay
7084769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay    private final File mOutDir;
71b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan    private final SharedPreferences mPref;
72b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan
73b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan    private final File[] mSlots = new File[NUM_OF_SLOTS];
74c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay    private int mNextSlot;
7584769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay
7684769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay    /**
7784769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay     * @param outDir see {@link #prepareStorage(File)}.
7884769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay     */
79b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan    public ClipStorage(File outDir, SharedPreferences pref) {
8084769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay        assert(outDir.isDirectory());
8184769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay        mOutDir = outDir;
82b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan        mPref = pref;
83b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan
84c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        mNextSlot = mPref.getInt(NEXT_AVAIL_SLOT, 0);
8584769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay    }
8684769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay
8784769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay    /**
88b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan     * Tries to get the next available clip slot. It's guaranteed to return one. If none of
89b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan     * slots is available, it returns the next slot of the most recently returned slot by this
90b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan     * method.
9184769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay     *
92b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan     * <p>This is not a perfect solution, but should be enough for most regular use. There are
93b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan     * several situations this method may not work:
94b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan     * <ul>
95b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan     *     <li>Making {@link #NUM_OF_SLOTS} - 1 times of large drag and drop or moveTo/copyTo/delete
96b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan     *     operations after cutting a primary clip, then the primary clip is overwritten.</li>
97b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan     *     <li>Having more than {@link #NUM_OF_SLOTS} queued jumbo file operations, one or more clip
98b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan     *     file may be overwritten.</li>
99b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan     * </ul>
100c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay     *
101c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay     * Implementations should take caution to serialize access.
10284769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay     */
103c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay    @VisibleForTesting
1049666ce691a4d45344460f17cec3577dbe075235aGarfield, Tan    synchronized int claimStorageSlot() {
105c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        int curSlot = mNextSlot;
106c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        for (int i = 0; i < NUM_OF_SLOTS; ++i, curSlot = (curSlot + 1) % NUM_OF_SLOTS) {
107c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            createSlotFileObject(curSlot);
108b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan
109c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            if (!mSlots[curSlot].exists()) {
110b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan                break;
111b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan            }
112b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan
113b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan            // No file or only primary file exists, we deem it available.
114c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            if (mSlots[curSlot].list().length <= 1) {
115b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan                break;
116b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan            }
117b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan            // This slot doesn't seem available, but still need to check if it's a legacy of
118b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan            // service being killed or a service crash etc. If it's stale, it's available.
119c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            else if (checkStaleFiles(curSlot)) {
120b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan                break;
121b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan            }
122b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan        }
123b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan
124c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        prepareSlot(curSlot);
125b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan
126c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        mNextSlot = (curSlot + 1) % NUM_OF_SLOTS;
127c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        mPref.edit().putInt(NEXT_AVAIL_SLOT, mNextSlot).commit();
128c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        return curSlot;
129b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan    }
130b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan
131b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan    private boolean checkStaleFiles(int pos) {
132b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan        File slotData = toSlotDataFile(pos);
133b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan
134b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan        // No need to check if the file exists. File.lastModified() returns 0L if the file doesn't
135b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan        // exist.
136b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan        return slotData.lastModified() + STALENESS_THRESHOLD <= System.currentTimeMillis();
137b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan    }
138b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan
139b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan    private void prepareSlot(int pos) {
140b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan        assert(mSlots[pos] != null);
141b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan
142b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan        Files.deleteRecursively(mSlots[pos]);
143b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan        mSlots[pos].mkdir();
144b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan        assert(mSlots[pos].isDirectory());
14584769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay    }
14684769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay
14784769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay    /**
148edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan     * Returns a writer. Callers must close the writer when finished.
14984769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay     */
150c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay    private Writer createWriter(int slot) throws IOException {
151c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        File file = toSlotDataFile(slot);
152edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan        return new Writer(file);
15384769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay    }
15484769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay
155c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay    @Override
156c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay    public synchronized File getFile(int slot) throws IOException {
157c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        createSlotFileObject(slot);
158b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan
159c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        File primary = toSlotDataFile(slot);
160b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan
161c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        String linkFileName = Integer.toString(mSlots[slot].list().length);
162c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        File link = new File(mSlots[slot], linkFileName);
163b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan
164b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan        try {
165b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan            Os.symlink(primary.getAbsolutePath(), link.getAbsolutePath());
166b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan        } catch (ErrnoException e) {
167b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan            e.rethrowAsIOException();
168b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan        }
169b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan        return link;
170b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan    }
171b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan
172c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay    @Override
173c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay    public ClipStorageReader createReader(File file) throws IOException {
174b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan        assert(file.getParentFile().getParentFile().equals(mOutDir));
1759666ce691a4d45344460f17cec3577dbe075235aGarfield, Tan        return new ClipStorageReader(file);
17684769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay    }
17784769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay
178b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan    private File toSlotDataFile(int pos) {
179b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan        assert(mSlots[pos] != null);
180b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan        return new File(mSlots[pos], PRIMARY_DATA_FILE_NAME);
18184769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay    }
18284769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay
18364936a7f5027a8592d227a3d058fd97b4d134dc1Garfield, Tan    private void createSlotFileObject(int pos) {
184b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan        if (mSlots[pos] == null) {
185b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan            mSlots[pos] = new File(mOutDir, Integer.toString(pos));
186b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan        }
18784769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay    }
18884769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay
189edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan    /**
190edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan     * Provides initialization of the clip data storage directory.
191edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan     */
1929666ce691a4d45344460f17cec3577dbe075235aGarfield, Tan    public static File prepareStorage(File cacheDir) {
193edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan        File clipDir = getClipDir(cacheDir);
194edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan        clipDir.mkdir();
195edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan
196edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan        assert(clipDir.isDirectory());
197edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan        return clipDir;
198edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan    }
199edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan
200edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan    private static File getClipDir(File cacheDir) {
201edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan        return new File(cacheDir, "clippings");
202edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan    }
203edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan
204c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay    public static final class Writer implements Closeable {
20584769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay
20684769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay        private final FileOutputStream mOut;
207edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan        private final FileLock mLock;
208edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan
209edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan        private Writer(File file) throws IOException {
210b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan            assert(!file.exists());
211b7e5f6b55f5075e4752bd94586211fae9af2bae0Garfield, Tan
212edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan            mOut = new FileOutputStream(file);
21384769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay
214edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan            // Lock the file here so copy tasks would wait until everything is flushed to disk
215edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan            // before start to run.
216edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan            mLock = mOut.getChannel().lock();
21784769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay        }
21884769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay
21984769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay        public void write(Uri uri) throws IOException {
22084769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay            mOut.write(uri.toString().getBytes());
22184769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay            mOut.write(LINE_SEPARATOR);
22284769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay        }
22384769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay
22484769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay        @Override
22584769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay        public void close() throws IOException {
226edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan            if (mLock != null) {
227edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan                mLock.release();
228edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan            }
229edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan
230edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan            if (mOut != null) {
231edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan                mOut.close();
232edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan            }
23384769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay        }
23484769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay    }
23584769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay
236c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay    @Override
237c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay    public int persistUris(Iterable<Uri> uris) {
238c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        int slot = claimStorageSlot();
239c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        persistUris(uris, slot);
240c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        return slot;
241c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay    }
242c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay
243c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay    @VisibleForTesting
244c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay    void persistUris(Iterable<Uri> uris, int slot) {
245c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        new PersistTask(this, uris, slot).execute();
246c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay    }
247c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay
24884769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay    /**
249edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan     * An {@link AsyncTask} that persists doc uris in {@link ClipStorage}.
25084769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay     */
251c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay    private static final class PersistTask extends AsyncTask<Void, Void, Void> {
252edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan
253c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        private final ClipStorage mClipStore;
254edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan        private final Iterable<Uri> mUris;
255c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        private final int mSlot;
256edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan
257c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay        PersistTask(ClipStorage clipStore, Iterable<Uri> uris, int slot) {
258c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            mClipStore = clipStore;
259edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan            mUris = uris;
260c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            mSlot = slot;
261edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan        }
262edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan
263edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan        @Override
264edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan        protected Void doInBackground(Void... params) {
265c8889af6757ecb348fb8fd6dadf84d67d3478cbfSteve McKay            try(Writer writer = mClipStore.createWriter(mSlot)){
266edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan                for (Uri uri: mUris) {
267edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan                    assert(uri != null);
268edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan                    writer.write(uri);
269edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan                }
270edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan            } catch (IOException e) {
271edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan                Log.e(TAG, "Caught exception trying to write jumbo clip to disk.", e);
272edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan            }
273edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan
274edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan            return null;
27584769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay        }
27684769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay    }
27784769b8205cf4a100c9d4f65c41cf72ba7a3b40fSteve McKay}
278