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