• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
DownloadCache(GalleryApp application, File root, long capacity)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 
findEntryInDatabase(String stringUrl)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 
download(JobContext jc, URL url)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 
updateLastAccess(long id)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 
freeSomeSpaceIfNeed(int maxDeleteFileCount)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 
insertEntry(String url, File file)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 
initialize()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 
DatabaseHelper(Context context)227         public DatabaseHelper(Context context) {
228             super(context, DATABASE_NAME, null, DATABASE_VERSION);
229         }
230 
231         @Override
onCreate(SQLiteDatabase db)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
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)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 
Entry(long id, File cacheFile)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 
DownloadTask(String url)265         public DownloadTask(String url) {
266             mUrl = Utils.checkNotNull(url);
267         }
268 
removeProxy(TaskProxy proxy)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
addProxy(TaskProxy proxy)280         public void addProxy(TaskProxy proxy) {
281             proxy.mTask = this;
282             mProxySet.add(proxy);
283         }
284 
285         @Override
onFutureDone(Future<File> future)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
run(JobContext jc)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 
setResult(Entry entry)342         synchronized void setResult(Entry entry) {
343             if (mIsCancelled) return;
344             mEntry = entry;
345             notifyAll();
346         }
347 
get(JobContext jc)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