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