| /* |
| * 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; |
| } |
| } |
| } |