Home | History | Annotate | Download | only in common
      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 
     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     @Override
     96     public void close() {
     97         mDbHelper.close();
     98     }
     99 
    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 
    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 
    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 
    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 
    189     public File createFile() throws IOException {
    190         return File.createTempFile(FILE_PREFIX, FILE_POSTFIX, mRootDir);
    191     }
    192 
    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 
    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
    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 
    289         public DatabaseHelper(Context context, String dbName) {
    290             super(context, dbName, null, DATABASE_VERSION);
    291         }
    292 
    293         @Override
    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
    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