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
17package com.android.providers.calendar;
18
19import android.content.ContentValues;
20import android.database.Cursor;
21import android.database.sqlite.SQLiteDatabase;
22import android.database.sqlite.SQLiteOpenHelper;
23import android.util.Log;
24import com.google.common.annotations.VisibleForTesting;
25
26import 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 */
32public 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