1 /* 2 * Copyright (C) 2011 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.gallery3d.common; 18 19 import android.content.ContentValues; 20 import android.content.Context; 21 import android.database.Cursor; 22 import android.database.sqlite.SQLiteDatabase; 23 import android.database.sqlite.SQLiteOpenHelper; 24 import android.util.Log; 25 26 import com.android.gallery3d.common.Entry.Table; 27 28 import java.io.Closeable; 29 import java.io.File; 30 import java.io.IOException; 31 32 public class FileCache implements Closeable { 33 private static final int LRU_CAPACITY = 4; 34 private static final int MAX_DELETE_COUNT = 16; 35 36 private static final String TAG = "FileCache"; 37 private static final String TABLE_NAME = FileEntry.SCHEMA.getTableName(); 38 private static final String FILE_PREFIX = "download"; 39 private static final String FILE_POSTFIX = ".tmp"; 40 41 private static final String QUERY_WHERE = 42 FileEntry.Columns.HASH_CODE + "=? AND " + FileEntry.Columns.CONTENT_URL + "=?"; 43 private static final String ID_WHERE = FileEntry.Columns.ID + "=?"; 44 private static final String[] PROJECTION_SIZE_SUM = 45 {String.format("sum(%s)", FileEntry.Columns.SIZE)}; 46 private static final String FREESPACE_PROJECTION[] = { 47 FileEntry.Columns.ID, FileEntry.Columns.FILENAME, 48 FileEntry.Columns.CONTENT_URL, FileEntry.Columns.SIZE}; 49 private static final String FREESPACE_ORDER_BY = 50 String.format("%s ASC", FileEntry.Columns.LAST_ACCESS); 51 52 private final LruCache<String, CacheEntry> mEntryMap = 53 new LruCache<String, CacheEntry>(LRU_CAPACITY); 54 55 private File mRootDir; 56 private long mCapacity; 57 private boolean mInitialized = false; 58 private long mTotalBytes; 59 60 private DatabaseHelper mDbHelper; 61 62 public static final class CacheEntry { 63 private long id; 64 public String contentUrl; 65 public File cacheFile; 66 CacheEntry(long id, String contentUrl, File cacheFile)67 private CacheEntry(long id, String contentUrl, File cacheFile) { 68 this.id = id; 69 this.contentUrl = contentUrl; 70 this.cacheFile = cacheFile; 71 } 72 } 73 deleteFiles(Context context, File rootDir, String dbName)74 public static void deleteFiles(Context context, File rootDir, String dbName) { 75 try { 76 context.getDatabasePath(dbName).delete(); 77 File[] files = rootDir.listFiles(); 78 if (files == null) return; 79 for (File file : rootDir.listFiles()) { 80 String name = file.getName(); 81 if (file.isFile() && name.startsWith(FILE_PREFIX) 82 && name.endsWith(FILE_POSTFIX)) file.delete(); 83 } 84 } catch (Throwable t) { 85 Log.w(TAG, "cannot reset database", t); 86 } 87 } 88 FileCache(Context context, File rootDir, String dbName, long capacity)89 public FileCache(Context context, File rootDir, String dbName, long capacity) { 90 mRootDir = Utils.checkNotNull(rootDir); 91 mCapacity = capacity; 92 mDbHelper = new DatabaseHelper(context, dbName); 93 } 94 95 @Override close()96 public void close() { 97 mDbHelper.close(); 98 } 99 store(String downloadUrl, File file)100 public void store(String downloadUrl, File file) { 101 if (!mInitialized) initialize(); 102 103 Utils.assertTrue(file.getParentFile().equals(mRootDir)); 104 FileEntry entry = new FileEntry(); 105 entry.hashCode = Utils.crc64Long(downloadUrl); 106 entry.contentUrl = downloadUrl; 107 entry.filename = file.getName(); 108 entry.size = file.length(); 109 entry.lastAccess = System.currentTimeMillis(); 110 if (entry.size >= mCapacity) { 111 file.delete(); 112 throw new IllegalArgumentException("file too large: " + entry.size); 113 } 114 synchronized (this) { 115 FileEntry original = queryDatabase(downloadUrl); 116 if (original != null) { 117 file.delete(); 118 entry.filename = original.filename; 119 entry.size = original.size; 120 } else { 121 mTotalBytes += entry.size; 122 } 123 FileEntry.SCHEMA.insertOrReplace( 124 mDbHelper.getWritableDatabase(), entry); 125 if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT); 126 } 127 } 128 lookup(String downloadUrl)129 public CacheEntry lookup(String downloadUrl) { 130 if (!mInitialized) initialize(); 131 CacheEntry entry; 132 synchronized (mEntryMap) { 133 entry = mEntryMap.get(downloadUrl); 134 } 135 136 if (entry != null) { 137 synchronized (this) { 138 updateLastAccess(entry.id); 139 } 140 return entry; 141 } 142 143 synchronized (this) { 144 FileEntry file = queryDatabase(downloadUrl); 145 if (file == null) return null; 146 entry = new CacheEntry( 147 file.id, downloadUrl, new File(mRootDir, file.filename)); 148 if (!entry.cacheFile.isFile()) { // file has been removed 149 try { 150 mDbHelper.getWritableDatabase().delete( 151 TABLE_NAME, ID_WHERE, new String[] {String.valueOf(file.id)}); 152 mTotalBytes -= file.size; 153 } catch (Throwable t) { 154 Log.w(TAG, "cannot delete entry: " + file.filename, t); 155 } 156 return null; 157 } 158 synchronized (mEntryMap) { 159 mEntryMap.put(downloadUrl, entry); 160 } 161 return entry; 162 } 163 } 164 queryDatabase(String downloadUrl)165 private FileEntry queryDatabase(String downloadUrl) { 166 long hash = Utils.crc64Long(downloadUrl); 167 String whereArgs[] = new String[] {String.valueOf(hash), downloadUrl}; 168 Cursor cursor = mDbHelper.getReadableDatabase().query(TABLE_NAME, 169 FileEntry.SCHEMA.getProjection(), 170 QUERY_WHERE, whereArgs, null, null, null); 171 try { 172 if (!cursor.moveToNext()) return null; 173 FileEntry entry = new FileEntry(); 174 FileEntry.SCHEMA.cursorToObject(cursor, entry); 175 updateLastAccess(entry.id); 176 return entry; 177 } finally { 178 cursor.close(); 179 } 180 } 181 updateLastAccess(long id)182 private void updateLastAccess(long id) { 183 ContentValues values = new ContentValues(); 184 values.put(FileEntry.Columns.LAST_ACCESS, System.currentTimeMillis()); 185 mDbHelper.getWritableDatabase().update(TABLE_NAME, 186 values, ID_WHERE, new String[] {String.valueOf(id)}); 187 } 188 createFile()189 public File createFile() throws IOException { 190 return File.createTempFile(FILE_PREFIX, FILE_POSTFIX, mRootDir); 191 } 192 initialize()193 private synchronized void initialize() { 194 if (mInitialized) return; 195 196 if (!mRootDir.isDirectory()) { 197 mRootDir.mkdirs(); 198 if (!mRootDir.isDirectory()) { 199 throw new RuntimeException("cannot create: " + mRootDir.getAbsolutePath()); 200 } 201 } 202 203 Cursor cursor = mDbHelper.getReadableDatabase().query( 204 TABLE_NAME, PROJECTION_SIZE_SUM, 205 null, null, null, null, null); 206 try { 207 if (cursor.moveToNext()) mTotalBytes = cursor.getLong(0); 208 } finally { 209 cursor.close(); 210 } 211 if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT); 212 213 // Mark initialized when everything above went through. If an exception was thrown, 214 // initialize() will be retried later. 215 mInitialized = true; 216 } 217 freeSomeSpaceIfNeed(int maxDeleteFileCount)218 private void freeSomeSpaceIfNeed(int maxDeleteFileCount) { 219 Cursor cursor = mDbHelper.getReadableDatabase().query( 220 TABLE_NAME, FREESPACE_PROJECTION, 221 null, null, null, null, FREESPACE_ORDER_BY); 222 try { 223 while (maxDeleteFileCount > 0 224 && mTotalBytes > mCapacity && cursor.moveToNext()) { 225 long id = cursor.getLong(0); 226 String path = cursor.getString(1); 227 String url = cursor.getString(2); 228 long size = cursor.getLong(3); 229 230 synchronized (mEntryMap) { 231 // if some one still uses it 232 if (mEntryMap.containsKey(url)) continue; 233 } 234 235 --maxDeleteFileCount; 236 if (new File(mRootDir, path).delete()) { 237 mTotalBytes -= size; 238 mDbHelper.getWritableDatabase().delete(TABLE_NAME, 239 ID_WHERE, new String[]{String.valueOf(id)}); 240 } else { 241 Log.w(TAG, "unable to delete file: " + path); 242 } 243 } 244 } finally { 245 cursor.close(); 246 } 247 } 248 249 @Table("files") 250 private static class FileEntry extends Entry { 251 public static final EntrySchema SCHEMA = new EntrySchema(FileEntry.class); 252 253 public interface Columns extends Entry.Columns { 254 public static final String HASH_CODE = "hash_code"; 255 public static final String CONTENT_URL = "content_url"; 256 public static final String FILENAME = "filename"; 257 public static final String SIZE = "size"; 258 public static final String LAST_ACCESS = "last_access"; 259 } 260 261 @Column(value = Columns.HASH_CODE, indexed = true) 262 public long hashCode; 263 264 @Column(Columns.CONTENT_URL) 265 public String contentUrl; 266 267 @Column(Columns.FILENAME) 268 public String filename; 269 270 @Column(Columns.SIZE) 271 public long size; 272 273 @Column(value = Columns.LAST_ACCESS, indexed = true) 274 public long lastAccess; 275 276 @Override toString()277 public String toString() { 278 return new StringBuilder() 279 .append("hash_code: ").append(hashCode).append(", ") 280 .append("content_url").append(contentUrl).append(", ") 281 .append("last_access").append(lastAccess).append(", ") 282 .append("filename").append(filename).toString(); 283 } 284 } 285 286 private final class DatabaseHelper extends SQLiteOpenHelper { 287 public static final int DATABASE_VERSION = 1; 288 DatabaseHelper(Context context, String dbName)289 public DatabaseHelper(Context context, String dbName) { 290 super(context, dbName, null, DATABASE_VERSION); 291 } 292 293 @Override onCreate(SQLiteDatabase db)294 public void onCreate(SQLiteDatabase db) { 295 FileEntry.SCHEMA.createTables(db); 296 297 // delete old files 298 for (File file : mRootDir.listFiles()) { 299 if (!file.delete()) { 300 Log.w(TAG, "fail to remove: " + file.getAbsolutePath()); 301 } 302 } 303 } 304 305 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)306 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 307 //reset everything 308 FileEntry.SCHEMA.dropTables(db); 309 onCreate(db); 310 } 311 } 312 } 313