summaryrefslogtreecommitdiffstats
path: root/src/com/android/gallery3d/data/DownloadCache.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/gallery3d/data/DownloadCache.java')
-rw-r--r--src/com/android/gallery3d/data/DownloadCache.java370
1 files changed, 370 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/data/DownloadCache.java b/src/com/android/gallery3d/data/DownloadCache.java
new file mode 100644
index 000000000..be7820b01
--- /dev/null
+++ b/src/com/android/gallery3d/data/DownloadCache.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.LruCache;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DownloadEntry.Columns;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.io.File;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.HashSet;
+
+public class DownloadCache {
+ private static final String TAG = "DownloadCache";
+ private static final int MAX_DELETE_COUNT = 16;
+ private static final int LRU_CAPACITY = 4;
+
+ private static final String TABLE_NAME = DownloadEntry.SCHEMA.getTableName();
+
+ private static final String QUERY_PROJECTION[] = {Columns.ID, Columns.DATA};
+ private static final String WHERE_HASH_AND_URL = String.format(
+ "%s = ? AND %s = ?", Columns.HASH_CODE, Columns.CONTENT_URL);
+ private static final int QUERY_INDEX_ID = 0;
+ private static final int QUERY_INDEX_DATA = 1;
+
+ private static final String FREESPACE_PROJECTION[] = {
+ Columns.ID, Columns.DATA, Columns.CONTENT_URL, Columns.CONTENT_SIZE};
+ private static final String FREESPACE_ORDER_BY =
+ String.format("%s ASC", Columns.LAST_ACCESS);
+ private static final int FREESPACE_IDNEX_ID = 0;
+ private static final int FREESPACE_IDNEX_DATA = 1;
+ private static final int FREESPACE_INDEX_CONTENT_URL = 2;
+ private static final int FREESPACE_INDEX_CONTENT_SIZE = 3;
+
+ private static final String ID_WHERE = Columns.ID + " = ?";
+
+ private static final String SUM_PROJECTION[] =
+ {String.format("sum(%s)", Columns.CONTENT_SIZE)};
+ private static final int SUM_INDEX_SUM = 0;
+
+ private final LruCache<String, Entry> mEntryMap =
+ new LruCache<String, Entry>(LRU_CAPACITY);
+ private final HashMap<String, DownloadTask> mTaskMap =
+ new HashMap<String, DownloadTask>();
+ private final File mRoot;
+ private final GalleryApp mApplication;
+ private final SQLiteDatabase mDatabase;
+ private final long mCapacity;
+
+ private long mTotalBytes = 0;
+ private boolean mInitialized = false;
+
+ public DownloadCache(GalleryApp application, File root, long capacity) {
+ mRoot = Utils.checkNotNull(root);
+ mApplication = Utils.checkNotNull(application);
+ mCapacity = capacity;
+ mDatabase = new DatabaseHelper(application.getAndroidContext())
+ .getWritableDatabase();
+ }
+
+ private Entry findEntryInDatabase(String stringUrl) {
+ long hash = Utils.crc64Long(stringUrl);
+ String whereArgs[] = {String.valueOf(hash), stringUrl};
+ Cursor cursor = mDatabase.query(TABLE_NAME, QUERY_PROJECTION,
+ WHERE_HASH_AND_URL, whereArgs, null, null, null);
+ try {
+ if (cursor.moveToNext()) {
+ File file = new File(cursor.getString(QUERY_INDEX_DATA));
+ long id = cursor.getInt(QUERY_INDEX_ID);
+ Entry entry = null;
+ synchronized (mEntryMap) {
+ entry = mEntryMap.get(stringUrl);
+ if (entry == null) {
+ entry = new Entry(id, file);
+ mEntryMap.put(stringUrl, entry);
+ }
+ }
+ return entry;
+ }
+ } finally {
+ cursor.close();
+ }
+ return null;
+ }
+
+ public Entry download(JobContext jc, URL url) {
+ if (!mInitialized) initialize();
+
+ String stringUrl = url.toString();
+
+ // First find in the entry-pool
+ synchronized (mEntryMap) {
+ Entry entry = mEntryMap.get(stringUrl);
+ if (entry != null) {
+ updateLastAccess(entry.mId);
+ return entry;
+ }
+ }
+
+ // Then, find it in database
+ TaskProxy proxy = new TaskProxy();
+ synchronized (mTaskMap) {
+ Entry entry = findEntryInDatabase(stringUrl);
+ if (entry != null) {
+ updateLastAccess(entry.mId);
+ return entry;
+ }
+
+ // Finally, we need to download the file ....
+ // First check if we are downloading it now ...
+ DownloadTask task = mTaskMap.get(stringUrl);
+ if (task == null) { // if not, start the download task now
+ task = new DownloadTask(stringUrl);
+ mTaskMap.put(stringUrl, task);
+ task.mFuture = mApplication.getThreadPool().submit(task, task);
+ }
+ task.addProxy(proxy);
+ }
+
+ return proxy.get(jc);
+ }
+
+ private void updateLastAccess(long id) {
+ ContentValues values = new ContentValues();
+ values.put(Columns.LAST_ACCESS, System.currentTimeMillis());
+ mDatabase.update(TABLE_NAME, values,
+ ID_WHERE, new String[] {String.valueOf(id)});
+ }
+
+ private synchronized void freeSomeSpaceIfNeed(int maxDeleteFileCount) {
+ if (mTotalBytes <= mCapacity) return;
+ Cursor cursor = mDatabase.query(TABLE_NAME,
+ FREESPACE_PROJECTION, null, null, null, null, FREESPACE_ORDER_BY);
+ try {
+ while (maxDeleteFileCount > 0
+ && mTotalBytes > mCapacity && cursor.moveToNext()) {
+ long id = cursor.getLong(FREESPACE_IDNEX_ID);
+ String url = cursor.getString(FREESPACE_INDEX_CONTENT_URL);
+ long size = cursor.getLong(FREESPACE_INDEX_CONTENT_SIZE);
+ String path = cursor.getString(FREESPACE_IDNEX_DATA);
+ boolean containsKey;
+ synchronized (mEntryMap) {
+ containsKey = mEntryMap.containsKey(url);
+ }
+ if (!containsKey) {
+ --maxDeleteFileCount;
+ mTotalBytes -= size;
+ new File(path).delete();
+ mDatabase.delete(TABLE_NAME,
+ ID_WHERE, new String[]{String.valueOf(id)});
+ } else {
+ // skip delete, since it is being used
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private synchronized long insertEntry(String url, File file) {
+ long size = file.length();
+ mTotalBytes += size;
+
+ ContentValues values = new ContentValues();
+ String hashCode = String.valueOf(Utils.crc64Long(url));
+ values.put(Columns.DATA, file.getAbsolutePath());
+ values.put(Columns.HASH_CODE, hashCode);
+ values.put(Columns.CONTENT_URL, url);
+ values.put(Columns.CONTENT_SIZE, size);
+ values.put(Columns.LAST_UPDATED, System.currentTimeMillis());
+ return mDatabase.insert(TABLE_NAME, "", values);
+ }
+
+ private synchronized void initialize() {
+ if (mInitialized) return;
+ mInitialized = true;
+ if (!mRoot.isDirectory()) mRoot.mkdirs();
+ if (!mRoot.isDirectory()) {
+ throw new RuntimeException("cannot create " + mRoot.getAbsolutePath());
+ }
+
+ Cursor cursor = mDatabase.query(
+ TABLE_NAME, SUM_PROJECTION, null, null, null, null, null);
+ mTotalBytes = 0;
+ try {
+ if (cursor.moveToNext()) {
+ mTotalBytes = cursor.getLong(SUM_INDEX_SUM);
+ }
+ } finally {
+ cursor.close();
+ }
+ if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT);
+ }
+
+ private final class DatabaseHelper extends SQLiteOpenHelper {
+ public static final String DATABASE_NAME = "download.db";
+ public static final int DATABASE_VERSION = 2;
+
+ public DatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ DownloadEntry.SCHEMA.createTables(db);
+ // Delete old files
+ for (File file : mRoot.listFiles()) {
+ if (!file.delete()) {
+ Log.w(TAG, "fail to remove: " + file.getAbsolutePath());
+ }
+ }
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ //reset everything
+ DownloadEntry.SCHEMA.dropTables(db);
+ onCreate(db);
+ }
+ }
+
+ public class Entry {
+ public File cacheFile;
+ protected long mId;
+
+ Entry(long id, File cacheFile) {
+ mId = id;
+ this.cacheFile = Utils.checkNotNull(cacheFile);
+ }
+ }
+
+ private class DownloadTask implements Job<File>, FutureListener<File> {
+ private HashSet<TaskProxy> mProxySet = new HashSet<TaskProxy>();
+ private Future<File> mFuture;
+ private final String mUrl;
+
+ public DownloadTask(String url) {
+ mUrl = Utils.checkNotNull(url);
+ }
+
+ public void removeProxy(TaskProxy proxy) {
+ synchronized (mTaskMap) {
+ Utils.assertTrue(mProxySet.remove(proxy));
+ if (mProxySet.isEmpty()) {
+ mFuture.cancel();
+ mTaskMap.remove(mUrl);
+ }
+ }
+ }
+
+ // should be used in synchronized block of mDatabase
+ public void addProxy(TaskProxy proxy) {
+ proxy.mTask = this;
+ mProxySet.add(proxy);
+ }
+
+ @Override
+ public void onFutureDone(Future<File> future) {
+ File file = future.get();
+ long id = 0;
+ if (file != null) { // insert to database
+ id = insertEntry(mUrl, file);
+ }
+
+ if (future.isCancelled()) {
+ Utils.assertTrue(mProxySet.isEmpty());
+ return;
+ }
+
+ synchronized (mTaskMap) {
+ Entry entry = null;
+ synchronized (mEntryMap) {
+ if (file != null) {
+ entry = new Entry(id, file);
+ Utils.assertTrue(mEntryMap.put(mUrl, entry) == null);
+ }
+ }
+ for (TaskProxy proxy : mProxySet) {
+ proxy.setResult(entry);
+ }
+ mTaskMap.remove(mUrl);
+ freeSomeSpaceIfNeed(MAX_DELETE_COUNT);
+ }
+ }
+
+ @Override
+ public File run(JobContext jc) {
+ // TODO: utilize etag
+ jc.setMode(ThreadPool.MODE_NETWORK);
+ File tempFile = null;
+ try {
+ URL url = new URL(mUrl);
+ tempFile = File.createTempFile("cache", ".tmp", mRoot);
+ // download from url to tempFile
+ jc.setMode(ThreadPool.MODE_NETWORK);
+ boolean downloaded = DownloadUtils.requestDownload(jc, url, tempFile);
+ jc.setMode(ThreadPool.MODE_NONE);
+ if (downloaded) return tempFile;
+ } catch (Exception e) {
+ Log.e(TAG, String.format("fail to download %s", mUrl), e);
+ } finally {
+ jc.setMode(ThreadPool.MODE_NONE);
+ }
+ if (tempFile != null) tempFile.delete();
+ return null;
+ }
+ }
+
+ public static class TaskProxy {
+ private DownloadTask mTask;
+ private boolean mIsCancelled = false;
+ private Entry mEntry;
+
+ synchronized void setResult(Entry entry) {
+ if (mIsCancelled) return;
+ mEntry = entry;
+ notifyAll();
+ }
+
+ public synchronized Entry get(JobContext jc) {
+ jc.setCancelListener(new CancelListener() {
+ @Override
+ public void onCancel() {
+ mTask.removeProxy(TaskProxy.this);
+ synchronized (TaskProxy.this) {
+ mIsCancelled = true;
+ TaskProxy.this.notifyAll();
+ }
+ }
+ });
+ while (!mIsCancelled && mEntry == null) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ Log.w(TAG, "ignore interrupt", e);
+ }
+ }
+ jc.setCancelListener(null);
+ return mEntry;
+ }
+ }
+}