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