| /* |
| * 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<String, CacheEntry> mEntryMap = |
| new LruCache<String, CacheEntry>(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); |
| } |
| } |
| } |