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 {
CacheException()65         public CacheException() {
66         }
67 
CacheException(String detailMessage)68         public CacheException(String detailMessage) {
69             super(detailMessage);
70         }
71     }
72 
CalendarCache(SQLiteOpenHelper openHelper)73     public CalendarCache(SQLiteOpenHelper openHelper) {
74         mOpenHelper = openHelper;
75     }
76 
writeTimezoneDatabaseVersion(String timezoneDatabaseVersion)77     public void writeTimezoneDatabaseVersion(String timezoneDatabaseVersion) throws CacheException {
78         writeData(KEY_TIMEZONE_DATABASE_VERSION, timezoneDatabaseVersion);
79     }
80 
readTimezoneDatabaseVersion()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
writeTimezoneType(String timezoneType)91     public void writeTimezoneType(String timezoneType) throws CacheException {
92         writeData(KEY_TIMEZONE_TYPE, timezoneType);
93     }
94 
readTimezoneType()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 
writeTimezoneInstances(String timezone)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 
readTimezoneInstances()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 
writeTimezoneInstancesPrevious(String timezone)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 
readTimezoneInstancesPrevious()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      */
writeData(String key, String value)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      */
writeDataLocked(SQLiteDatabase db, String key, String value)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      */
readData(String key)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      */
readDataLocked(SQLiteDatabase db, String key)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