/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.gallery3d.common; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; import com.android.gallery3d.common.Entry.Table; import java.io.Closeable; import java.io.File; import java.io.IOException; public class FileCache implements Closeable { private static final int LRU_CAPACITY = 4; private static final int MAX_DELETE_COUNT = 16; private static final String TAG = "FileCache"; private static final String TABLE_NAME = FileEntry.SCHEMA.getTableName(); private static final String FILE_PREFIX = "download"; private static final String FILE_POSTFIX = ".tmp"; private static final String QUERY_WHERE = FileEntry.Columns.HASH_CODE + "=? AND " + FileEntry.Columns.CONTENT_URL + "=?"; private static final String ID_WHERE = FileEntry.Columns.ID + "=?"; private static final String[] PROJECTION_SIZE_SUM = {String.format("sum(%s)", FileEntry.Columns.SIZE)}; private static final String FREESPACE_PROJECTION[] = { FileEntry.Columns.ID, FileEntry.Columns.FILENAME, FileEntry.Columns.CONTENT_URL, FileEntry.Columns.SIZE}; private static final String FREESPACE_ORDER_BY = String.format("%s ASC", FileEntry.Columns.LAST_ACCESS); private final LruCache mEntryMap = new LruCache(LRU_CAPACITY); private File mRootDir; private long mCapacity; private boolean mInitialized = false; private long mTotalBytes; private DatabaseHelper mDbHelper; public static final class CacheEntry { private long id; public String contentUrl; public File cacheFile; private CacheEntry(long id, String contentUrl, File cacheFile) { this.id = id; this.contentUrl = contentUrl; this.cacheFile = cacheFile; } } public static void deleteFiles(Context context, File rootDir, String dbName) { try { context.getDatabasePath(dbName).delete(); File[] files = rootDir.listFiles(); if (files == null) return; for (File file : rootDir.listFiles()) { String name = file.getName(); if (file.isFile() && name.startsWith(FILE_PREFIX) && name.endsWith(FILE_POSTFIX)) file.delete(); } } catch (Throwable t) { Log.w(TAG, "cannot reset database", t); } } public FileCache(Context context, File rootDir, String dbName, long capacity) { mRootDir = Utils.checkNotNull(rootDir); mCapacity = capacity; mDbHelper = new DatabaseHelper(context, dbName); } @Override public void close() { mDbHelper.close(); } public void store(String downloadUrl, File file) { if (!mInitialized) initialize(); Utils.assertTrue(file.getParentFile().equals(mRootDir)); FileEntry entry = new FileEntry(); entry.hashCode = Utils.crc64Long(downloadUrl); entry.contentUrl = downloadUrl; entry.filename = file.getName(); entry.size = file.length(); entry.lastAccess = System.currentTimeMillis(); if (entry.size >= mCapacity) { file.delete(); throw new IllegalArgumentException("file too large: " + entry.size); } synchronized (this) { FileEntry original = queryDatabase(downloadUrl); if (original != null) { file.delete(); entry.filename = original.filename; entry.size = original.size; } else { mTotalBytes += entry.size; } FileEntry.SCHEMA.insertOrReplace( mDbHelper.getWritableDatabase(), entry); if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT); } } public CacheEntry lookup(String downloadUrl) { if (!mInitialized) initialize(); CacheEntry entry; synchronized (mEntryMap) { entry = mEntryMap.get(downloadUrl); } if (entry != null) { synchronized (this) { updateLastAccess(entry.id); } return entry; } synchronized (this) { FileEntry file = queryDatabase(downloadUrl); if (file == null) return null; entry = new CacheEntry( file.id, downloadUrl, new File(mRootDir, file.filename)); if (!entry.cacheFile.isFile()) { // file has been removed try { mDbHelper.getWritableDatabase().delete( TABLE_NAME, ID_WHERE, new String[] {String.valueOf(file.id)}); mTotalBytes -= file.size; } catch (Throwable t) { Log.w(TAG, "cannot delete entry: " + file.filename, t); } return null; } synchronized (mEntryMap) { mEntryMap.put(downloadUrl, entry); } return entry; } } private FileEntry queryDatabase(String downloadUrl) { long hash = Utils.crc64Long(downloadUrl); String whereArgs[] = new String[] {String.valueOf(hash), downloadUrl}; Cursor cursor = mDbHelper.getReadableDatabase().query(TABLE_NAME, FileEntry.SCHEMA.getProjection(), QUERY_WHERE, whereArgs, null, null, null); try { if (!cursor.moveToNext()) return null; FileEntry entry = new FileEntry(); FileEntry.SCHEMA.cursorToObject(cursor, entry); updateLastAccess(entry.id); return entry; } finally { cursor.close(); } } private void updateLastAccess(long id) { ContentValues values = new ContentValues(); values.put(FileEntry.Columns.LAST_ACCESS, System.currentTimeMillis()); mDbHelper.getWritableDatabase().update(TABLE_NAME, values, ID_WHERE, new String[] {String.valueOf(id)}); } public File createFile() throws IOException { return File.createTempFile(FILE_PREFIX, FILE_POSTFIX, mRootDir); } private synchronized void initialize() { if (mInitialized) return; if (!mRootDir.isDirectory()) { mRootDir.mkdirs(); if (!mRootDir.isDirectory()) { throw new RuntimeException("cannot create: " + mRootDir.getAbsolutePath()); } } Cursor cursor = mDbHelper.getReadableDatabase().query( TABLE_NAME, PROJECTION_SIZE_SUM, null, null, null, null, null); try { if (cursor.moveToNext()) mTotalBytes = cursor.getLong(0); } finally { cursor.close(); } if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT); // Mark initialized when everything above went through. If an exception was thrown, // initialize() will be retried later. mInitialized = true; } private void freeSomeSpaceIfNeed(int maxDeleteFileCount) { Cursor cursor = mDbHelper.getReadableDatabase().query( TABLE_NAME, FREESPACE_PROJECTION, null, null, null, null, FREESPACE_ORDER_BY); try { while (maxDeleteFileCount > 0 && mTotalBytes > mCapacity && cursor.moveToNext()) { long id = cursor.getLong(0); String path = cursor.getString(1); String url = cursor.getString(2); long size = cursor.getLong(3); synchronized (mEntryMap) { // if some one still uses it if (mEntryMap.containsKey(url)) continue; } --maxDeleteFileCount; if (new File(mRootDir, path).delete()) { mTotalBytes -= size; mDbHelper.getWritableDatabase().delete(TABLE_NAME, ID_WHERE, new String[]{String.valueOf(id)}); } else { Log.w(TAG, "unable to delete file: " + path); } } } finally { cursor.close(); } } @Table("files") private static class FileEntry extends Entry { public static final EntrySchema SCHEMA = new EntrySchema(FileEntry.class); public interface Columns extends Entry.Columns { public static final String HASH_CODE = "hash_code"; public static final String CONTENT_URL = "content_url"; public static final String FILENAME = "filename"; public static final String SIZE = "size"; public static final String LAST_ACCESS = "last_access"; } @Column(value = Columns.HASH_CODE, indexed = true) public long hashCode; @Column(Columns.CONTENT_URL) public String contentUrl; @Column(Columns.FILENAME) public String filename; @Column(Columns.SIZE) public long size; @Column(value = Columns.LAST_ACCESS, indexed = true) public long lastAccess; @Override public String toString() { return new StringBuilder() .append("hash_code: ").append(hashCode).append(", ") .append("content_url").append(contentUrl).append(", ") .append("last_access").append(lastAccess).append(", ") .append("filename").append(filename).toString(); } } private final class DatabaseHelper extends SQLiteOpenHelper { public static final int DATABASE_VERSION = 1; public DatabaseHelper(Context context, String dbName) { super(context, dbName, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { FileEntry.SCHEMA.createTables(db); // delete old files for (File file : mRootDir.listFiles()) { if (!file.delete()) { Log.w(TAG, "fail to remove: " + file.getAbsolutePath()); } } } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { //reset everything FileEntry.SCHEMA.dropTables(db); onCreate(db); } } }