Home | History | Annotate | Download | only in data
      1 /*
      2  * Copyright (C) 2010 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.data;
     18 
     19 import com.android.gallery3d.app.GalleryApp;
     20 import com.android.gallery3d.common.LruCache;
     21 import com.android.gallery3d.common.Utils;
     22 import com.android.gallery3d.data.DownloadEntry.Columns;
     23 import com.android.gallery3d.util.Future;
     24 import com.android.gallery3d.util.FutureListener;
     25 import com.android.gallery3d.util.ThreadPool;
     26 import com.android.gallery3d.util.ThreadPool.CancelListener;
     27 import com.android.gallery3d.util.ThreadPool.Job;
     28 import com.android.gallery3d.util.ThreadPool.JobContext;
     29 
     30 import android.content.ContentValues;
     31 import android.content.Context;
     32 import android.database.Cursor;
     33 import android.database.sqlite.SQLiteDatabase;
     34 import android.database.sqlite.SQLiteOpenHelper;
     35 
     36 import java.io.File;
     37 import java.net.URL;
     38 import java.util.HashMap;
     39 import java.util.HashSet;
     40 import java.util.WeakHashMap;
     41 
     42 public class DownloadCache {
     43     private static final String TAG = "DownloadCache";
     44     private static final int MAX_DELETE_COUNT = 16;
     45     private static final int LRU_CAPACITY = 4;
     46 
     47     private static final String TABLE_NAME = DownloadEntry.SCHEMA.getTableName();
     48 
     49     private static final String QUERY_PROJECTION[] = {Columns.ID, Columns.DATA};
     50     private static final String WHERE_HASH_AND_URL = String.format(
     51             "%s = ? AND %s = ?", Columns.HASH_CODE, Columns.CONTENT_URL);
     52     private static final int QUERY_INDEX_ID = 0;
     53     private static final int QUERY_INDEX_DATA = 1;
     54 
     55     private static final String FREESPACE_PROJECTION[] = {
     56             Columns.ID, Columns.DATA, Columns.CONTENT_URL, Columns.CONTENT_SIZE};
     57     private static final String FREESPACE_ORDER_BY =
     58             String.format("%s ASC", Columns.LAST_ACCESS);
     59     private static final int FREESPACE_IDNEX_ID = 0;
     60     private static final int FREESPACE_IDNEX_DATA = 1;
     61     private static final int FREESPACE_INDEX_CONTENT_URL = 2;
     62     private static final int FREESPACE_INDEX_CONTENT_SIZE = 3;
     63 
     64     private static final String ID_WHERE = Columns.ID + " = ?";
     65 
     66     private static final String SUM_PROJECTION[] =
     67             {String.format("sum(%s)", Columns.CONTENT_SIZE)};
     68     private static final int SUM_INDEX_SUM = 0;
     69 
     70     private final LruCache<String, Entry> mEntryMap =
     71             new LruCache<String, Entry>(LRU_CAPACITY);
     72     private final HashMap<String, DownloadTask> mTaskMap =
     73             new HashMap<String, DownloadTask>();
     74     private final File mRoot;
     75     private final GalleryApp mApplication;
     76     private final SQLiteDatabase mDatabase;
     77     private final long mCapacity;
     78 
     79     private long mTotalBytes = 0;
     80     private boolean mInitialized = false;
     81     private WeakHashMap<Object, Entry> mAssociateMap = new WeakHashMap<Object, Entry>();
     82 
     83     public DownloadCache(GalleryApp application, File root, long capacity) {
     84         mRoot = Utils.checkNotNull(root);
     85         mApplication = Utils.checkNotNull(application);
     86         mCapacity = capacity;
     87         mDatabase = new DatabaseHelper(application.getAndroidContext())
     88                 .getWritableDatabase();
     89     }
     90 
     91     private Entry findEntryInDatabase(String stringUrl) {
     92         long hash = Utils.crc64Long(stringUrl);
     93         String whereArgs[] = {String.valueOf(hash), stringUrl};
     94         Cursor cursor = mDatabase.query(TABLE_NAME, QUERY_PROJECTION,
     95                 WHERE_HASH_AND_URL, whereArgs, null, null, null);
     96         try {
     97             if (cursor.moveToNext()) {
     98                 File file = new File(cursor.getString(QUERY_INDEX_DATA));
     99                 long id = cursor.getInt(QUERY_INDEX_ID);
    100                 Entry entry = null;
    101                 synchronized (mEntryMap) {
    102                     entry = mEntryMap.get(stringUrl);
    103                     if (entry == null) {
    104                         entry = new Entry(id, file);
    105                         mEntryMap.put(stringUrl, entry);
    106                     }
    107                 }
    108                 return entry;
    109             }
    110         } finally {
    111             cursor.close();
    112         }
    113         return null;
    114     }
    115 
    116     public Entry lookup(URL url) {
    117         if (!mInitialized) initialize();
    118         String stringUrl = url.toString();
    119 
    120         // First find in the entry-pool
    121         synchronized (mEntryMap) {
    122             Entry entry = mEntryMap.get(stringUrl);
    123             if (entry != null) {
    124                 updateLastAccess(entry.mId);
    125                 return entry;
    126             }
    127         }
    128 
    129         // Then, find it in database
    130         TaskProxy proxy = new TaskProxy();
    131         synchronized (mTaskMap) {
    132             Entry entry = findEntryInDatabase(stringUrl);
    133             if (entry != null) {
    134                 updateLastAccess(entry.mId);
    135                 return entry;
    136             }
    137         }
    138         return null;
    139     }
    140 
    141     public Entry download(JobContext jc, URL url) {
    142         if (!mInitialized) initialize();
    143 
    144         String stringUrl = url.toString();
    145 
    146         // First find in the entry-pool
    147         synchronized (mEntryMap) {
    148             Entry entry = mEntryMap.get(stringUrl);
    149             if (entry != null) {
    150                 updateLastAccess(entry.mId);
    151                 return entry;
    152             }
    153         }
    154 
    155         // Then, find it in database
    156         TaskProxy proxy = new TaskProxy();
    157         synchronized (mTaskMap) {
    158             Entry entry = findEntryInDatabase(stringUrl);
    159             if (entry != null) {
    160                 updateLastAccess(entry.mId);
    161                 return entry;
    162             }
    163 
    164             // Finally, we need to download the file ....
    165             // First check if we are downloading it now ...
    166             DownloadTask task = mTaskMap.get(stringUrl);
    167             if (task == null) { // if not, start the download task now
    168                 task = new DownloadTask(stringUrl);
    169                 mTaskMap.put(stringUrl, task);
    170                 task.mFuture = mApplication.getThreadPool().submit(task, task);
    171             }
    172             task.addProxy(proxy);
    173         }
    174 
    175         return proxy.get(jc);
    176     }
    177 
    178     private void updateLastAccess(long id) {
    179         ContentValues values = new ContentValues();
    180         values.put(Columns.LAST_ACCESS, System.currentTimeMillis());
    181         mDatabase.update(TABLE_NAME, values,
    182                 ID_WHERE, new String[] {String.valueOf(id)});
    183     }
    184 
    185     private synchronized void freeSomeSpaceIfNeed(int maxDeleteFileCount) {
    186         if (mTotalBytes <= mCapacity) return;
    187         Cursor cursor = mDatabase.query(TABLE_NAME,
    188                 FREESPACE_PROJECTION, null, null, null, null, FREESPACE_ORDER_BY);
    189         try {
    190             while (maxDeleteFileCount > 0
    191                     && mTotalBytes > mCapacity && cursor.moveToNext()) {
    192                 long id = cursor.getLong(FREESPACE_IDNEX_ID);
    193                 String url = cursor.getString(FREESPACE_INDEX_CONTENT_URL);
    194                 long size = cursor.getLong(FREESPACE_INDEX_CONTENT_SIZE);
    195                 String path = cursor.getString(FREESPACE_IDNEX_DATA);
    196                 boolean containsKey;
    197                 synchronized (mEntryMap) {
    198                     containsKey = mEntryMap.containsKey(url);
    199                 }
    200                 if (!containsKey) {
    201                     --maxDeleteFileCount;
    202                     mTotalBytes -= size;
    203                     new File(path).delete();
    204                     mDatabase.delete(TABLE_NAME,
    205                             ID_WHERE, new String[]{String.valueOf(id)});
    206                 } else {
    207                     // skip delete, since it is being used
    208                 }
    209             }
    210         } finally {
    211             cursor.close();
    212         }
    213     }
    214 
    215     private synchronized long insertEntry(String url, File file) {
    216         long size = file.length();
    217         mTotalBytes += size;
    218 
    219         ContentValues values = new ContentValues();
    220         String hashCode = String.valueOf(Utils.crc64Long(url));
    221         values.put(Columns.DATA, file.getAbsolutePath());
    222         values.put(Columns.HASH_CODE, hashCode);
    223         values.put(Columns.CONTENT_URL, url);
    224         values.put(Columns.CONTENT_SIZE, size);
    225         values.put(Columns.LAST_UPDATED, System.currentTimeMillis());
    226         return mDatabase.insert(TABLE_NAME, "", values);
    227     }
    228 
    229     private synchronized void initialize() {
    230         if (mInitialized) return;
    231         mInitialized = true;
    232         if (!mRoot.isDirectory()) mRoot.mkdirs();
    233         if (!mRoot.isDirectory()) {
    234             throw new RuntimeException("cannot create " + mRoot.getAbsolutePath());
    235         }
    236 
    237         Cursor cursor = mDatabase.query(
    238                 TABLE_NAME, SUM_PROJECTION, null, null, null, null, null);
    239         mTotalBytes = 0;
    240         try {
    241             if (cursor.moveToNext()) {
    242                 mTotalBytes = cursor.getLong(SUM_INDEX_SUM);
    243             }
    244         } finally {
    245             cursor.close();
    246         }
    247         if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT);
    248     }
    249 
    250     private final class DatabaseHelper extends SQLiteOpenHelper {
    251         public static final String DATABASE_NAME = "download.db";
    252         public static final int DATABASE_VERSION = 2;
    253 
    254         public DatabaseHelper(Context context) {
    255             super(context, DATABASE_NAME, null, DATABASE_VERSION);
    256         }
    257 
    258         @Override
    259         public void onCreate(SQLiteDatabase db) {
    260             DownloadEntry.SCHEMA.createTables(db);
    261             // Delete old files
    262             for (File file : mRoot.listFiles()) {
    263                 if (!file.delete()) {
    264                     Log.w(TAG, "fail to remove: " + file.getAbsolutePath());
    265                 }
    266             }
    267         }
    268 
    269         @Override
    270         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    271             //reset everything
    272             DownloadEntry.SCHEMA.dropTables(db);
    273             onCreate(db);
    274         }
    275     }
    276 
    277     public class Entry {
    278         public File cacheFile;
    279         protected long mId;
    280 
    281         Entry(long id, File cacheFile) {
    282             mId = id;
    283             this.cacheFile = Utils.checkNotNull(cacheFile);
    284         }
    285 
    286         public void associateWith(Object object) {
    287             mAssociateMap.put(Utils.checkNotNull(object), this);
    288         }
    289     }
    290 
    291     private class DownloadTask implements Job<File>, FutureListener<File> {
    292         private HashSet<TaskProxy> mProxySet = new HashSet<TaskProxy>();
    293         private Future<File> mFuture;
    294         private final String mUrl;
    295 
    296         public DownloadTask(String url) {
    297             mUrl = Utils.checkNotNull(url);
    298         }
    299 
    300         public void removeProxy(TaskProxy proxy) {
    301             synchronized (mTaskMap) {
    302                 Utils.assertTrue(mProxySet.remove(proxy));
    303                 if (mProxySet.isEmpty()) {
    304                     mFuture.cancel();
    305                     mTaskMap.remove(mUrl);
    306                 }
    307             }
    308         }
    309 
    310         // should be used in synchronized block of mDatabase
    311         public void addProxy(TaskProxy proxy) {
    312             proxy.mTask = this;
    313             mProxySet.add(proxy);
    314         }
    315 
    316         public void onFutureDone(Future<File> future) {
    317             File file = future.get();
    318             long id = 0;
    319             if (file != null) { // insert to database
    320                 id = insertEntry(mUrl, file);
    321             }
    322 
    323             if (future.isCancelled()) {
    324                 Utils.assertTrue(mProxySet.isEmpty());
    325                 return;
    326             }
    327 
    328             synchronized (mTaskMap) {
    329                 Entry entry = null;
    330                 synchronized (mEntryMap) {
    331                     if (file != null) {
    332                         entry = new Entry(id, file);
    333                         Utils.assertTrue(mEntryMap.put(mUrl, entry) == null);
    334                     }
    335                 }
    336                 for (TaskProxy proxy : mProxySet) {
    337                     proxy.setResult(entry);
    338                 }
    339                 mTaskMap.remove(mUrl);
    340                 freeSomeSpaceIfNeed(MAX_DELETE_COUNT);
    341             }
    342         }
    343 
    344         public File run(JobContext jc) {
    345             // TODO: utilize etag
    346             jc.setMode(ThreadPool.MODE_NETWORK);
    347             File tempFile = null;
    348             try {
    349                 URL url = new URL(mUrl);
    350                 tempFile = File.createTempFile("cache", ".tmp", mRoot);
    351                 // download from url to tempFile
    352                 jc.setMode(ThreadPool.MODE_NETWORK);
    353                 boolean downloaded = DownloadUtils.requestDownload(jc, url, tempFile);
    354                 jc.setMode(ThreadPool.MODE_NONE);
    355                 if (downloaded) return tempFile;
    356             } catch (Exception e) {
    357                 Log.e(TAG, String.format("fail to download %s", mUrl), e);
    358             } finally {
    359                 jc.setMode(ThreadPool.MODE_NONE);
    360             }
    361             if (tempFile != null) tempFile.delete();
    362             return null;
    363         }
    364     }
    365 
    366     public static class TaskProxy {
    367         private DownloadTask mTask;
    368         private boolean mIsCancelled = false;
    369         private Entry mEntry;
    370 
    371         synchronized void setResult(Entry entry) {
    372             if (mIsCancelled) return;
    373             mEntry = entry;
    374             notifyAll();
    375         }
    376 
    377         public synchronized Entry get(JobContext jc) {
    378             jc.setCancelListener(new CancelListener() {
    379                 public void onCancel() {
    380                     mTask.removeProxy(TaskProxy.this);
    381                     synchronized (TaskProxy.this) {
    382                         mIsCancelled = true;
    383                         TaskProxy.this.notifyAll();
    384                     }
    385                 }
    386             });
    387             while (!mIsCancelled && mEntry == null) {
    388                 try {
    389                     wait();
    390                 } catch (InterruptedException e) {
    391                     Log.w(TAG, "ignore interrupt", e);
    392                 }
    393             }
    394             jc.setCancelListener(null);
    395             return mEntry;
    396         }
    397     }
    398 }
    399