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.providers.calendar; 18 19 import android.content.ContentValues; 20 import android.database.Cursor; 21 import android.database.sqlite.SQLiteDatabase; 22 import android.database.sqlite.SQLiteOpenHelper; 23 import android.util.Log; 24 import com.google.common.annotations.VisibleForTesting; 25 26 import java.util.TimeZone; 27 28 /** 29 * Class for managing a persistent Cache of (key, value) pairs. The persistent storage used is 30 * a SQLite database. 31 */ 32 public class CalendarCache { 33 private static final String TAG = "CalendarCache"; 34 35 public static final String DATABASE_NAME = "CalendarCache"; 36 37 public static final String KEY_TIMEZONE_DATABASE_VERSION = "timezoneDatabaseVersion"; 38 public static final String DEFAULT_TIMEZONE_DATABASE_VERSION = "2009s"; 39 40 public static final String KEY_TIMEZONE_TYPE = "timezoneType"; 41 public static final String TIMEZONE_TYPE_AUTO = "auto"; 42 public static final String TIMEZONE_TYPE_HOME = "home"; 43 44 public static final String KEY_TIMEZONE_INSTANCES = "timezoneInstances"; 45 public static final String KEY_TIMEZONE_INSTANCES_PREVIOUS = "timezoneInstancesPrevious"; 46 47 public static final String COLUMN_NAME_ID = "_id"; 48 public static final String COLUMN_NAME_KEY = "key"; 49 public static final String COLUMN_NAME_VALUE = "value"; 50 51 private static final String[] sProjection = { 52 COLUMN_NAME_KEY, 53 COLUMN_NAME_VALUE 54 }; 55 56 private static final int COLUMN_INDEX_KEY = 0; 57 private static final int COLUMN_INDEX_VALUE = 1; 58 59 private final SQLiteOpenHelper mOpenHelper; 60 61 /** 62 * This exception is thrown when the cache encounter a null key or a null database reference 63 */ 64 public static class CacheException extends Exception { 65 public CacheException() { 66 } 67 68 public CacheException(String detailMessage) { 69 super(detailMessage); 70 } 71 } 72 73 public CalendarCache(SQLiteOpenHelper openHelper) { 74 mOpenHelper = openHelper; 75 } 76 77 public void writeTimezoneDatabaseVersion(String timezoneDatabaseVersion) throws CacheException { 78 writeData(KEY_TIMEZONE_DATABASE_VERSION, timezoneDatabaseVersion); 79 } 80 81 public String readTimezoneDatabaseVersion() { 82 try { 83 return readData(KEY_TIMEZONE_DATABASE_VERSION); 84 } catch (CacheException e) { 85 Log.e(TAG, "Could not read timezone database version from CalendarCache"); 86 } 87 return null; 88 } 89 90 @VisibleForTesting 91 public void writeTimezoneType(String timezoneType) throws CacheException { 92 writeData(KEY_TIMEZONE_TYPE, timezoneType); 93 } 94 95 public String readTimezoneType() { 96 try { 97 return readData(KEY_TIMEZONE_TYPE); 98 } catch (CacheException e) { 99 Log.e(TAG, "Cannot read timezone type from CalendarCache - using AUTO as default", e); 100 } 101 return TIMEZONE_TYPE_AUTO; 102 } 103 104 public void writeTimezoneInstances(String timezone) { 105 try { 106 writeData(KEY_TIMEZONE_INSTANCES, timezone); 107 } catch (CacheException e) { 108 Log.e(TAG, "Cannot write instances timezone to CalendarCache"); 109 } 110 } 111 112 public String readTimezoneInstances() { 113 try { 114 return readData(KEY_TIMEZONE_INSTANCES); 115 } catch (CacheException e) { 116 String localTimezone = TimeZone.getDefault().getID(); 117 Log.e(TAG, "Cannot read instances timezone from CalendarCache - using device one: " + 118 localTimezone, e); 119 return localTimezone; 120 } 121 } 122 123 public void writeTimezoneInstancesPrevious(String timezone) { 124 try { 125 writeData(KEY_TIMEZONE_INSTANCES_PREVIOUS, timezone); 126 } catch (CacheException e) { 127 Log.e(TAG, "Cannot write previous instance timezone to CalendarCache"); 128 } 129 } 130 131 public String readTimezoneInstancesPrevious() { 132 try { 133 return readData(KEY_TIMEZONE_INSTANCES_PREVIOUS); 134 } catch (CacheException e) { 135 Log.e(TAG, "Cannot read previous instances timezone from CalendarCache", e); 136 } 137 return null; 138 } 139 140 /** 141 * Write a (key, value) pair in the Cache. 142 * 143 * @param key the key (must not be null) 144 * @param value the value (can be null) 145 * @throws CacheException when key is null 146 */ 147 public void writeData(String key, String value) throws CacheException { 148 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 149 db.beginTransaction(); 150 try { 151 writeDataLocked(db, key, value); 152 db.setTransactionSuccessful(); 153 if (Log.isLoggable(TAG, Log.VERBOSE)) { 154 Log.i(TAG, "Wrote (key, value) = [ " + key + ", " + value + "] "); 155 } 156 } finally { 157 db.endTransaction(); 158 } 159 } 160 161 /** 162 * Write a (key, value) pair in the database used by the cache. This method should be called in 163 * a transaction. 164 * 165 * @param db the database (must not be null) 166 * @param key the key (must not be null) 167 * @param value the value 168 * @throws CacheException when key or database are null 169 */ 170 protected void writeDataLocked(SQLiteDatabase db, String key, String value) 171 throws CacheException { 172 if (null == db) { 173 throw new CacheException("Database cannot be null"); 174 } 175 if (null == key) { 176 throw new CacheException("Cannot use null key for write"); 177 } 178 179 /* 180 * Storing the hash code of a String into the _id column carries a (very) small risk 181 * of weird behavior, because we're using it as a unique key, but hash codes aren't 182 * guaranteed to be unique. CalendarCache has a small set of keys that are known 183 * ahead of time, so we should be okay. 184 */ 185 ContentValues values = new ContentValues(); 186 values.put(COLUMN_NAME_ID, key.hashCode()); 187 values.put(COLUMN_NAME_KEY, key); 188 values.put(COLUMN_NAME_VALUE, value); 189 190 db.replace(DATABASE_NAME, null /* null column hack */, values); 191 } 192 193 /** 194 * Read a value from the database used by the cache and depending on a key. 195 * 196 * @param key the key from which we want the value (must not be null) 197 * @return the value that was found for the key. Can be null if no key has been found 198 * @throws CacheException when key is null 199 */ 200 public String readData(String key) throws CacheException { 201 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 202 return readDataLocked(db, key); 203 } 204 205 /** 206 * Read a value from the database used by the cache and depending on a key. The database should 207 * be "readable" at minimum 208 * 209 * @param db the database (must not be null) 210 * @param key the key from which we want the value (must not be null) 211 * @return the value that was found for the key. Can be null if no value has been found for the 212 * key. 213 * @throws CacheException when key or database are null 214 */ 215 protected String readDataLocked(SQLiteDatabase db, String key) throws CacheException { 216 if (null == db) { 217 throw new CacheException("Database cannot be null"); 218 } 219 if (null == key) { 220 throw new CacheException("Cannot use null key for read"); 221 } 222 223 String rowValue = null; 224 225 Cursor cursor = db.query(DATABASE_NAME, sProjection, 226 COLUMN_NAME_KEY + "=?", new String[] { key }, null, null, null); 227 try { 228 if (cursor.moveToNext()) { 229 rowValue = cursor.getString(COLUMN_INDEX_VALUE); 230 } 231 else { 232 if (Log.isLoggable(TAG, Log.VERBOSE)) { 233 Log.i(TAG, "Could not find key = [ " + key + " ]"); 234 } 235 } 236 } finally { 237 cursor.close(); 238 cursor = null; 239 } 240 return rowValue; 241 } 242 } 243