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 17package com.android.gallery3d.common; 18 19import android.content.ContentValues; 20import android.content.Context; 21import android.database.Cursor; 22import android.database.sqlite.SQLiteDatabase; 23import android.database.sqlite.SQLiteOpenHelper; 24import android.util.Log; 25 26import com.android.gallery3d.common.Entry.Table; 27 28import java.io.Closeable; 29import java.io.File; 30import java.io.IOException; 31 32public 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 67 private CacheEntry(long id, String contentUrl, File cacheFile) { 68 this.id = id; 69 this.contentUrl = contentUrl; 70 this.cacheFile = cacheFile; 71 } 72 } 73 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 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 public void close() { 96 mDbHelper.close(); 97 } 98 99 public void store(String downloadUrl, File file) { 100 if (!mInitialized) initialize(); 101 102 Utils.assertTrue(file.getParentFile().equals(mRootDir)); 103 FileEntry entry = new FileEntry(); 104 entry.hashCode = Utils.crc64Long(downloadUrl); 105 entry.contentUrl = downloadUrl; 106 entry.filename = file.getName(); 107 entry.size = file.length(); 108 entry.lastAccess = System.currentTimeMillis(); 109 if (entry.size >= mCapacity) { 110 file.delete(); 111 throw new IllegalArgumentException("file too large: " + entry.size); 112 } 113 synchronized (this) { 114 FileEntry original = queryDatabase(downloadUrl); 115 if (original != null) { 116 file.delete(); 117 entry.filename = original.filename; 118 entry.size = original.size; 119 } else { 120 mTotalBytes += entry.size; 121 } 122 FileEntry.SCHEMA.insertOrReplace( 123 mDbHelper.getWritableDatabase(), entry); 124 if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT); 125 } 126 } 127 128 public CacheEntry lookup(String downloadUrl) { 129 if (!mInitialized) initialize(); 130 CacheEntry entry; 131 synchronized (mEntryMap) { 132 entry = mEntryMap.get(downloadUrl); 133 } 134 135 if (entry != null) { 136 synchronized (this) { 137 updateLastAccess(entry.id); 138 } 139 return entry; 140 } 141 142 synchronized (this) { 143 FileEntry file = queryDatabase(downloadUrl); 144 if (file == null) return null; 145 entry = new CacheEntry( 146 file.id, downloadUrl, new File(mRootDir, file.filename)); 147 if (!entry.cacheFile.isFile()) { // file has been removed 148 try { 149 mDbHelper.getWritableDatabase().delete( 150 TABLE_NAME, ID_WHERE, new String[] {String.valueOf(file.id)}); 151 mTotalBytes -= file.size; 152 } catch (Throwable t) { 153 Log.w(TAG, "cannot delete entry: " + file.filename, t); 154 } 155 return null; 156 } 157 synchronized (mEntryMap) { 158 mEntryMap.put(downloadUrl, entry); 159 } 160 return entry; 161 } 162 } 163 164 private FileEntry queryDatabase(String downloadUrl) { 165 long hash = Utils.crc64Long(downloadUrl); 166 String whereArgs[] = new String[] {String.valueOf(hash), downloadUrl}; 167 Cursor cursor = mDbHelper.getReadableDatabase().query(TABLE_NAME, 168 FileEntry.SCHEMA.getProjection(), 169 QUERY_WHERE, whereArgs, null, null, null); 170 try { 171 if (!cursor.moveToNext()) return null; 172 FileEntry entry = new FileEntry(); 173 FileEntry.SCHEMA.cursorToObject(cursor, entry); 174 updateLastAccess(entry.id); 175 return entry; 176 } finally { 177 cursor.close(); 178 } 179 } 180 181 private void updateLastAccess(long id) { 182 ContentValues values = new ContentValues(); 183 values.put(FileEntry.Columns.LAST_ACCESS, System.currentTimeMillis()); 184 mDbHelper.getWritableDatabase().update(TABLE_NAME, 185 values, ID_WHERE, new String[] {String.valueOf(id)}); 186 } 187 188 public File createFile() throws IOException { 189 return File.createTempFile(FILE_PREFIX, FILE_POSTFIX, mRootDir); 190 } 191 192 private synchronized void initialize() { 193 if (mInitialized) return; 194 mInitialized = true; 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 214 private void freeSomeSpaceIfNeed(int maxDeleteFileCount) { 215 Cursor cursor = mDbHelper.getReadableDatabase().query( 216 TABLE_NAME, FREESPACE_PROJECTION, 217 null, null, null, null, FREESPACE_ORDER_BY); 218 try { 219 while (maxDeleteFileCount > 0 220 && mTotalBytes > mCapacity && cursor.moveToNext()) { 221 long id = cursor.getLong(0); 222 String path = cursor.getString(1); 223 String url = cursor.getString(2); 224 long size = cursor.getLong(3); 225 226 synchronized (mEntryMap) { 227 // if some one still uses it 228 if (mEntryMap.containsKey(url)) continue; 229 } 230 231 --maxDeleteFileCount; 232 if (new File(mRootDir, path).delete()) { 233 mTotalBytes -= size; 234 mDbHelper.getWritableDatabase().delete(TABLE_NAME, 235 ID_WHERE, new String[]{String.valueOf(id)}); 236 } else { 237 Log.w(TAG, "unable to delete file: " + path); 238 } 239 } 240 } finally { 241 cursor.close(); 242 } 243 } 244 245 @Table("files") 246 private static class FileEntry extends Entry { 247 public static final EntrySchema SCHEMA = new EntrySchema(FileEntry.class); 248 249 public interface Columns extends Entry.Columns { 250 public static final String HASH_CODE = "hash_code"; 251 public static final String CONTENT_URL = "content_url"; 252 public static final String FILENAME = "filename"; 253 public static final String SIZE = "size"; 254 public static final String LAST_ACCESS = "last_access"; 255 } 256 257 @Column(value = Columns.HASH_CODE, indexed = true) 258 public long hashCode; 259 260 @Column(Columns.CONTENT_URL) 261 public String contentUrl; 262 263 @Column(Columns.FILENAME) 264 public String filename; 265 266 @Column(Columns.SIZE) 267 public long size; 268 269 @Column(value = Columns.LAST_ACCESS, indexed = true) 270 public long lastAccess; 271 272 @Override 273 public String toString() { 274 return new StringBuilder() 275 .append("hash_code: ").append(hashCode).append(", ") 276 .append("content_url").append(contentUrl).append(", ") 277 .append("last_access").append(lastAccess).append(", ") 278 .append("filename").append(filename).toString(); 279 } 280 } 281 282 private final class DatabaseHelper extends SQLiteOpenHelper { 283 public static final int DATABASE_VERSION = 1; 284 285 public DatabaseHelper(Context context, String dbName) { 286 super(context, dbName, null, DATABASE_VERSION); 287 } 288 289 @Override 290 public void onCreate(SQLiteDatabase db) { 291 FileEntry.SCHEMA.createTables(db); 292 293 // delete old files 294 for (File file : mRootDir.listFiles()) { 295 if (!file.delete()) { 296 Log.w(TAG, "fail to remove: " + file.getAbsolutePath()); 297 } 298 } 299 } 300 301 @Override 302 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 303 //reset everything 304 FileEntry.SCHEMA.dropTables(db); 305 onCreate(db); 306 } 307 } 308} 309