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