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     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 
    195         if (!mRootDir.isDirectory()) {
    196             mRootDir.mkdirs();
    197             if (!mRootDir.isDirectory()) {
    198                 throw new RuntimeException("cannot create: " + mRootDir.getAbsolutePath());
    199             }
    200         }
    201 
    202         Cursor cursor = mDbHelper.getReadableDatabase().query(
    203                 TABLE_NAME, PROJECTION_SIZE_SUM,
    204                 null, null, null, null, null);
    205         try {
    206             if (cursor.moveToNext()) mTotalBytes = cursor.getLong(0);
    207         } finally {
    208             cursor.close();
    209         }
    210         if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT);
    211 
    212         // Mark initialized when everything above went through. If an exception was thrown,
    213         // initialize() will be retried later.
    214         mInitialized = true;
    215     }
    216 
    217     private void freeSomeSpaceIfNeed(int maxDeleteFileCount) {
    218         Cursor cursor = mDbHelper.getReadableDatabase().query(
    219                 TABLE_NAME, FREESPACE_PROJECTION,
    220                 null, null, null, null, FREESPACE_ORDER_BY);
    221         try {
    222             while (maxDeleteFileCount > 0
    223                     && mTotalBytes > mCapacity && cursor.moveToNext()) {
    224                 long id = cursor.getLong(0);
    225                 String path = cursor.getString(1);
    226                 String url = cursor.getString(2);
    227                 long size = cursor.getLong(3);
    228 
    229                 synchronized (mEntryMap) {
    230                     // if some one still uses it
    231                     if (mEntryMap.containsKey(url)) continue;
    232                 }
    233 
    234                 --maxDeleteFileCount;
    235                 if (new File(mRootDir, path).delete()) {
    236                     mTotalBytes -= size;
    237                     mDbHelper.getWritableDatabase().delete(TABLE_NAME,
    238                             ID_WHERE, new String[]{String.valueOf(id)});
    239                 } else {
    240                     Log.w(TAG, "unable to delete file: " + path);
    241                 }
    242             }
    243         } finally {
    244             cursor.close();
    245         }
    246     }
    247 
    248     @Table("files")
    249     private static class FileEntry extends Entry {
    250         public static final EntrySchema SCHEMA = new EntrySchema(FileEntry.class);
    251 
    252         public interface Columns extends Entry.Columns {
    253             public static final String HASH_CODE = "hash_code";
    254             public static final String CONTENT_URL = "content_url";
    255             public static final String FILENAME = "filename";
    256             public static final String SIZE = "size";
    257             public static final String LAST_ACCESS = "last_access";
    258         }
    259 
    260         @Column(value = Columns.HASH_CODE, indexed = true)
    261         public long hashCode;
    262 
    263         @Column(Columns.CONTENT_URL)
    264         public String contentUrl;
    265 
    266         @Column(Columns.FILENAME)
    267         public String filename;
    268 
    269         @Column(Columns.SIZE)
    270         public long size;
    271 
    272         @Column(value = Columns.LAST_ACCESS, indexed = true)
    273         public long lastAccess;
    274 
    275         @Override
    276         public String toString() {
    277             return new StringBuilder()
    278                     .append("hash_code: ").append(hashCode).append(", ")
    279                     .append("content_url").append(contentUrl).append(", ")
    280                     .append("last_access").append(lastAccess).append(", ")
    281                     .append("filename").append(filename).toString();
    282         }
    283     }
    284 
    285     private final class DatabaseHelper extends SQLiteOpenHelper {
    286         public static final int DATABASE_VERSION = 1;
    287 
    288         public DatabaseHelper(Context context, String dbName) {
    289             super(context, dbName, null, DATABASE_VERSION);
    290         }
    291 
    292         @Override
    293         public void onCreate(SQLiteDatabase db) {
    294             FileEntry.SCHEMA.createTables(db);
    295 
    296             // delete old files
    297             for (File file : mRootDir.listFiles()) {
    298                 if (!file.delete()) {
    299                     Log.w(TAG, "fail to remove: " + file.getAbsolutePath());
    300                 }
    301             }
    302         }
    303 
    304         @Override
    305         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    306             //reset everything
    307             FileEntry.SCHEMA.dropTables(db);
    308             onCreate(db);
    309         }
    310     }
    311 }
    312