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 androidx.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 */ ClipStorage(File outDir, SharedPreferences pref)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 claimStorageSlot()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 checkStaleFiles(int pos)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 prepareSlot(int pos)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 */ createWriter(int slot)150 private Writer createWriter(int slot) throws IOException { 151 File file = toSlotDataFile(slot); 152 return new Writer(file); 153 } 154 155 @Override getFile(int slot)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 IOException newException = new IOException(e.getMessage()); 168 newException.initCause(e); 169 throw newException; 170 } 171 return link; 172 } 173 174 @Override createReader(File file)175 public ClipStorageReader createReader(File file) throws IOException { 176 assert(file.getParentFile().getParentFile().equals(mOutDir)); 177 return new ClipStorageReader(file); 178 } 179 toSlotDataFile(int pos)180 private File toSlotDataFile(int pos) { 181 assert(mSlots[pos] != null); 182 return new File(mSlots[pos], PRIMARY_DATA_FILE_NAME); 183 } 184 createSlotFileObject(int pos)185 private void createSlotFileObject(int pos) { 186 if (mSlots[pos] == null) { 187 mSlots[pos] = new File(mOutDir, Integer.toString(pos)); 188 } 189 } 190 191 /** 192 * Provides initialization of the clip data storage directory. 193 */ prepareStorage(File cacheDir)194 public static File prepareStorage(File cacheDir) { 195 File clipDir = getClipDir(cacheDir); 196 clipDir.mkdir(); 197 198 assert(clipDir.isDirectory()); 199 return clipDir; 200 } 201 getClipDir(File cacheDir)202 private static File getClipDir(File cacheDir) { 203 return new File(cacheDir, "clippings"); 204 } 205 206 public static final class Writer implements Closeable { 207 208 private final FileOutputStream mOut; 209 private final FileLock mLock; 210 Writer(File file)211 private Writer(File file) throws IOException { 212 assert(!file.exists()); 213 214 mOut = new FileOutputStream(file); 215 216 // Lock the file here so copy tasks would wait until everything is flushed to disk 217 // before start to run. 218 mLock = mOut.getChannel().lock(); 219 } 220 write(Uri uri)221 public void write(Uri uri) throws IOException { 222 mOut.write(uri.toString().getBytes()); 223 mOut.write(LINE_SEPARATOR); 224 } 225 226 @Override close()227 public void close() throws IOException { 228 if (mLock != null) { 229 mLock.release(); 230 } 231 232 if (mOut != null) { 233 mOut.close(); 234 } 235 } 236 } 237 238 @Override persistUris(Iterable<Uri> uris)239 public int persistUris(Iterable<Uri> uris) { 240 int slot = claimStorageSlot(); 241 persistUris(uris, slot); 242 return slot; 243 } 244 245 @VisibleForTesting persistUris(Iterable<Uri> uris, int slot)246 void persistUris(Iterable<Uri> uris, int slot) { 247 new PersistTask(this, uris, slot).execute(); 248 } 249 250 /** 251 * An {@link AsyncTask} that persists doc uris in {@link ClipStorage}. 252 */ 253 private static final class PersistTask extends AsyncTask<Void, Void, Void> { 254 255 private final ClipStorage mClipStore; 256 private final Iterable<Uri> mUris; 257 private final int mSlot; 258 PersistTask(ClipStorage clipStore, Iterable<Uri> uris, int slot)259 PersistTask(ClipStorage clipStore, Iterable<Uri> uris, int slot) { 260 mClipStore = clipStore; 261 mUris = uris; 262 mSlot = slot; 263 } 264 265 @Override doInBackground(Void... params)266 protected Void doInBackground(Void... params) { 267 try(Writer writer = mClipStore.createWriter(mSlot)){ 268 for (Uri uri: mUris) { 269 assert(uri != null); 270 writer.write(uri); 271 } 272 } catch (IOException e) { 273 Log.e(TAG, "Caught exception trying to write jumbo clip to disk.", e); 274 } 275 276 return null; 277 } 278 } 279 } 280