1 /* 2 * Copyright (C) 2013 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 package com.android.photos.data; 17 18 import android.content.ContentValues; 19 import android.content.Context; 20 import android.database.Cursor; 21 import android.database.sqlite.SQLiteDatabase; 22 import android.database.sqlite.SQLiteOpenHelper; 23 import android.net.Uri; 24 import android.provider.BaseColumns; 25 26 import com.android.photos.data.MediaRetriever.MediaSize; 27 28 import java.io.File; 29 30 class MediaCacheDatabase extends SQLiteOpenHelper { 31 public static final int DB_VERSION = 1; 32 public static final String DB_NAME = "mediacache.db"; 33 34 /** Internal database table used for the media cache */ 35 public static final String TABLE = "media_cache"; 36 37 private static interface Columns extends BaseColumns { 38 /** The Content URI of the original image. */ 39 public static final String URI = "uri"; 40 /** MediaSize.getValue() values. */ 41 public static final String MEDIA_SIZE = "media_size"; 42 /** The last time this image was queried. */ 43 public static final String LAST_ACCESS = "last_access"; 44 /** The image size in bytes. */ 45 public static final String SIZE_IN_BYTES = "size"; 46 } 47 48 static interface Action { 49 void execute(Uri uri, long id, MediaSize size, Object parameter); 50 } 51 52 private static final String[] PROJECTION_ID = { 53 Columns._ID, 54 }; 55 56 private static final String[] PROJECTION_CACHED = { 57 Columns._ID, Columns.MEDIA_SIZE, Columns.SIZE_IN_BYTES, 58 }; 59 60 private static final String[] PROJECTION_CACHE_SIZE = { 61 "SUM(" + Columns.SIZE_IN_BYTES + ")" 62 }; 63 64 private static final String[] PROJECTION_DELETE_OLD = { 65 Columns._ID, Columns.URI, Columns.MEDIA_SIZE, Columns.SIZE_IN_BYTES, Columns.LAST_ACCESS, 66 }; 67 68 public static final String CREATE_TABLE = "CREATE TABLE " + TABLE + "(" 69 + Columns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " 70 + Columns.URI + " TEXT NOT NULL," 71 + Columns.MEDIA_SIZE + " INTEGER NOT NULL," 72 + Columns.LAST_ACCESS + " INTEGER NOT NULL," 73 + Columns.SIZE_IN_BYTES + " INTEGER NOT NULL," 74 + "UNIQUE(" + Columns.URI + ", " + Columns.MEDIA_SIZE + "))"; 75 76 public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE; 77 78 public static final String WHERE_THUMBNAIL = Columns.MEDIA_SIZE + " = " 79 + MediaSize.Thumbnail.getValue(); 80 81 public static final String WHERE_NOT_THUMBNAIL = Columns.MEDIA_SIZE + " <> " 82 + MediaSize.Thumbnail.getValue(); 83 84 public static final String WHERE_CLEAR_CACHE = Columns.LAST_ACCESS + " <= ?"; 85 86 public static final String WHERE_CLEAR_CACHE_LARGE = WHERE_CLEAR_CACHE + " AND " 87 + WHERE_NOT_THUMBNAIL; 88 89 static class QueryCacheResults { 90 public QueryCacheResults(long id, int sizeVal) { 91 this.id = id; 92 this.size = MediaSize.fromInteger(sizeVal); 93 } 94 public long id; 95 public MediaSize size; 96 } 97 98 public MediaCacheDatabase(Context context) { 99 super(context, DB_NAME, null, DB_VERSION); 100 } 101 102 @Override 103 public void onCreate(SQLiteDatabase db) { 104 db.execSQL(CREATE_TABLE); 105 } 106 107 @Override 108 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 109 db.execSQL(DROP_TABLE); 110 onCreate(db); 111 MediaCache.getInstance().clearCacheDir(); 112 } 113 114 public Long getCached(Uri uri, MediaSize size) { 115 String where = Columns.URI + " = ? AND " + Columns.MEDIA_SIZE + " = ?"; 116 SQLiteDatabase db = getWritableDatabase(); 117 String[] whereArgs = { 118 uri.toString(), String.valueOf(size.getValue()), 119 }; 120 Cursor cursor = db.query(TABLE, PROJECTION_ID, where, whereArgs, null, null, null); 121 Long id = null; 122 if (cursor.moveToNext()) { 123 id = cursor.getLong(0); 124 } 125 cursor.close(); 126 if (id != null) { 127 String[] updateArgs = { 128 id.toString() 129 }; 130 ContentValues values = new ContentValues(); 131 values.put(Columns.LAST_ACCESS, System.currentTimeMillis()); 132 db.beginTransaction(); 133 try { 134 db.update(TABLE, values, Columns._ID + " = ?", updateArgs); 135 db.setTransactionSuccessful(); 136 } finally { 137 db.endTransaction(); 138 } 139 } 140 return id; 141 } 142 143 public MediaSize executeOnBestCached(Uri uri, MediaSize size, Action action) { 144 String where = Columns.URI + " = ? AND " + Columns.MEDIA_SIZE + " < ?"; 145 String orderBy = Columns.MEDIA_SIZE + " DESC"; 146 SQLiteDatabase db = getReadableDatabase(); 147 String[] whereArgs = { 148 uri.toString(), String.valueOf(size.getValue()), 149 }; 150 Cursor cursor = db.query(TABLE, PROJECTION_CACHED, where, whereArgs, null, null, orderBy); 151 MediaSize bestSize = null; 152 if (cursor.moveToNext()) { 153 long id = cursor.getLong(0); 154 bestSize = MediaSize.fromInteger(cursor.getInt(1)); 155 long fileSize = cursor.getLong(2); 156 action.execute(uri, id, bestSize, fileSize); 157 } 158 cursor.close(); 159 return bestSize; 160 } 161 162 public long insert(Uri uri, MediaSize size, Action action, File tempFile) { 163 SQLiteDatabase db = getWritableDatabase(); 164 db.beginTransaction(); 165 try { 166 ContentValues values = new ContentValues(); 167 values.put(Columns.LAST_ACCESS, System.currentTimeMillis()); 168 values.put(Columns.MEDIA_SIZE, size.getValue()); 169 values.put(Columns.URI, uri.toString()); 170 values.put(Columns.SIZE_IN_BYTES, tempFile.length()); 171 long id = db.insert(TABLE, null, values); 172 if (id != -1) { 173 action.execute(uri, id, size, tempFile); 174 db.setTransactionSuccessful(); 175 } 176 return id; 177 } finally { 178 db.endTransaction(); 179 } 180 } 181 182 public void updateLength(long id, long fileSize) { 183 ContentValues values = new ContentValues(); 184 values.put(Columns.SIZE_IN_BYTES, fileSize); 185 String[] whereArgs = { 186 String.valueOf(id) 187 }; 188 SQLiteDatabase db = getWritableDatabase(); 189 db.beginTransaction(); 190 try { 191 db.update(TABLE, values, Columns._ID + " = ?", whereArgs); 192 db.setTransactionSuccessful(); 193 } finally { 194 db.endTransaction(); 195 } 196 } 197 198 public void delete(Uri uri, MediaSize size, Action action) { 199 String where = Columns.URI + " = ? AND " + Columns.MEDIA_SIZE + " = ?"; 200 String[] whereArgs = { 201 uri.toString(), String.valueOf(size.getValue()), 202 }; 203 deleteRows(uri, where, whereArgs, action); 204 } 205 206 public void delete(Uri uri, Action action) { 207 String where = Columns.URI + " = ?"; 208 String[] whereArgs = { 209 uri.toString() 210 }; 211 deleteRows(uri, where, whereArgs, action); 212 } 213 214 private void deleteRows(Uri uri, String where, String[] whereArgs, Action action) { 215 SQLiteDatabase db = getWritableDatabase(); 216 // Make this an atomic operation 217 db.beginTransaction(); 218 Cursor cursor = db.query(TABLE, PROJECTION_CACHED, where, whereArgs, null, null, null); 219 while (cursor.moveToNext()) { 220 long id = cursor.getLong(0); 221 MediaSize size = MediaSize.fromInteger(cursor.getInt(1)); 222 long length = cursor.getLong(2); 223 action.execute(uri, id, size, length); 224 } 225 cursor.close(); 226 try { 227 db.delete(TABLE, where, whereArgs); 228 db.setTransactionSuccessful(); 229 } finally { 230 db.endTransaction(); 231 } 232 } 233 234 public void deleteOldCached(boolean includeThumbnails, long deleteSize, Action action) { 235 String where = includeThumbnails ? null : WHERE_NOT_THUMBNAIL; 236 long lastAccess = 0; 237 SQLiteDatabase db = getWritableDatabase(); 238 db.beginTransaction(); 239 try { 240 Cursor cursor = db.query(TABLE, PROJECTION_DELETE_OLD, where, null, null, null, 241 Columns.LAST_ACCESS); 242 while (cursor.moveToNext()) { 243 long id = cursor.getLong(0); 244 String uri = cursor.getString(1); 245 MediaSize size = MediaSize.fromInteger(cursor.getInt(2)); 246 long length = cursor.getLong(3); 247 long imageLastAccess = cursor.getLong(4); 248 249 if (imageLastAccess != lastAccess && deleteSize < 0) { 250 break; // We've deleted enough. 251 } 252 lastAccess = imageLastAccess; 253 action.execute(Uri.parse(uri), id, size, length); 254 deleteSize -= length; 255 } 256 cursor.close(); 257 String[] whereArgs = { 258 String.valueOf(lastAccess), 259 }; 260 String whereDelete = includeThumbnails ? WHERE_CLEAR_CACHE : WHERE_CLEAR_CACHE_LARGE; 261 db.delete(TABLE, whereDelete, whereArgs); 262 db.setTransactionSuccessful(); 263 } finally { 264 db.endTransaction(); 265 } 266 } 267 268 public long getCacheSize() { 269 return getCacheSize(null); 270 } 271 272 public long getThumbnailCacheSize() { 273 return getCacheSize(WHERE_THUMBNAIL); 274 } 275 276 private long getCacheSize(String where) { 277 SQLiteDatabase db = getReadableDatabase(); 278 Cursor cursor = db.query(TABLE, PROJECTION_CACHE_SIZE, where, null, null, null, null); 279 long size = -1; 280 if (cursor.moveToNext()) { 281 size = cursor.getLong(0); 282 } 283 cursor.close(); 284 return size; 285 } 286 } 287