1/*
2**
3** Copyright 2008, The Android Open Source Project
4**
5** Licensed under the Apache License, Version 2.0 (the "License");
6** you may not use this file except in compliance with the License.
7** You may obtain a copy of the License at
8**
9**     http://www.apache.org/licenses/LICENSE-2.0
10**
11** Unless required by applicable law or agreed to in writing, software
12** distributed under the License is distributed on an "AS IS" BASIS,
13** See the License for the specific language governing permissions and
14** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15** limitations under the License.
16*/
17
18package com.android.providers.calendar;
19
20
21import android.content.ContentValues;
22import android.database.Cursor;
23import android.database.sqlite.SQLiteDatabase;
24import android.database.sqlite.SQLiteOpenHelper;
25import android.provider.CalendarContract.CalendarMetaData;
26
27/**
28 * The global meta-data used for expanding the Instances table is stored in one
29 * row of the "CalendarMetaData" table.  This class is used for caching those
30 * values to avoid repeatedly banging on the database.  It is also used
31 * for writing the values back to the database, while maintaining the
32 * consistency of the cache.
33 * <p>
34 * TODO: there must be only one of these active within CalendarProvider.  Enforce this.
35 */
36public class MetaData {
37    /**
38     * These fields are updated atomically with the database.
39     * If fields are added or removed from the CalendarMetaData table, those
40     * changes must also be reflected here.
41     */
42    public class Fields {
43        public String timezone;     // local timezone used for Instance expansion
44        public long minInstance;    // UTC millis
45        public long maxInstance;    // UTC millis
46    }
47
48    /**
49     * The cached copy of the meta-data fields from the database.
50     */
51    private Fields mFields = new Fields();
52
53    private final SQLiteOpenHelper mOpenHelper;
54    private boolean mInitialized;
55
56    /**
57     * The column names in the CalendarMetaData table.  This projection
58     * must contain all of the columns.
59     */
60    private static final String[] sCalendarMetaDataProjection = {
61        CalendarMetaData.LOCAL_TIMEZONE,
62        CalendarMetaData.MIN_INSTANCE,
63        CalendarMetaData.MAX_INSTANCE};
64
65    private static final int METADATA_INDEX_LOCAL_TIMEZONE = 0;
66    private static final int METADATA_INDEX_MIN_INSTANCE = 1;
67    private static final int METADATA_INDEX_MAX_INSTANCE = 2;
68
69    public MetaData(SQLiteOpenHelper openHelper) {
70        mOpenHelper = openHelper;
71    }
72
73    /**
74     * Returns a copy of all the MetaData fields.  This method grabs a
75     * database lock to read all the fields atomically.
76     *
77     * @return a copy of all the MetaData fields.
78     */
79    public Fields getFields() {
80        Fields fields = new Fields();
81        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
82        db.beginTransaction();
83        try {
84            // If the fields have not been initialized from the database,
85            // then read the database.
86            if (!mInitialized) {
87                readLocked(db);
88            }
89            fields.timezone = mFields.timezone;
90            fields.minInstance = mFields.minInstance;
91            fields.maxInstance = mFields.maxInstance;
92            db.setTransactionSuccessful();
93        } finally {
94            db.endTransaction();
95        }
96        return fields;
97    }
98
99    /**
100     * This method must be called only while holding a database lock.
101     *
102     * <p>
103     * Returns a copy of all the MetaData fields.  This method assumes
104     * the database lock has already been acquired.
105     * </p>
106     *
107     * @return a copy of all the MetaData fields.
108     */
109    public Fields getFieldsLocked() {
110        Fields fields = new Fields();
111
112        // If the fields have not been initialized from the database,
113        // then read the database.
114        if (!mInitialized) {
115            SQLiteDatabase db = mOpenHelper.getReadableDatabase();
116            readLocked(db);
117        }
118        fields.timezone = mFields.timezone;
119        fields.minInstance = mFields.minInstance;
120        fields.maxInstance = mFields.maxInstance;
121        return fields;
122    }
123
124    /**
125     * Reads the meta-data for the CalendarProvider from the database and
126     * updates the member variables.  This method executes while the database
127     * lock is held.  If there were no exceptions reading the database,
128     * mInitialized is set to true.
129     */
130    private void readLocked(SQLiteDatabase db) {
131        String timezone = null;
132        long minInstance = 0, maxInstance = 0;
133
134        // Read the database directly.  We only do this once to initialize
135        // the members of this class.
136        Cursor cursor = db.query("CalendarMetaData", sCalendarMetaDataProjection,
137                null, null, null, null, null);
138        try {
139            if (cursor.moveToNext()) {
140                timezone = cursor.getString(METADATA_INDEX_LOCAL_TIMEZONE);
141                minInstance = cursor.getLong(METADATA_INDEX_MIN_INSTANCE);
142                maxInstance = cursor.getLong(METADATA_INDEX_MAX_INSTANCE);
143            }
144        } finally {
145            if (cursor != null) {
146                cursor.close();
147            }
148        }
149
150        // Cache the result of reading the database
151        mFields.timezone = timezone;
152        mFields.minInstance = minInstance;
153        mFields.maxInstance = maxInstance;
154
155        // Mark the fields as initialized
156        mInitialized = true;
157    }
158
159    /**
160     * Writes the meta-data for the CalendarProvider.  The values to write are
161     * passed in as parameters.  All of the values are updated atomically,
162     * including the cached copy of the meta-data.
163     *
164     * @param timezone the local timezone used for Instance expansion
165     * @param begin the start of the Instance expansion in UTC milliseconds
166     * @param end the end of the Instance expansion in UTC milliseconds
167     * @param startDay the start of the BusyBit expansion (the start Julian day)
168     * @param endDay the end of the BusyBit expansion (the end Julian day)
169     */
170    public void write(String timezone, long begin, long end, int startDay, int endDay) {
171        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
172        db.beginTransaction();
173        try {
174            writeLocked(timezone, begin, end);
175            db.setTransactionSuccessful();
176        } finally {
177            db.endTransaction();
178        }
179    }
180
181    /**
182     * This method must be called only while holding a database lock.
183     *
184     * <p>
185     * Writes the meta-data for the CalendarProvider.  The values to write are
186     * passed in as parameters.  All of the values are updated atomically,
187     * including the cached copy of the meta-data.
188     * </p>
189     *
190     * @param timezone the local timezone used for Instance expansion
191     * @param begin the start of the Instance expansion in UTC milliseconds
192     * @param end the end of the Instance expansion in UTC milliseconds
193     */
194    public void writeLocked(String timezone, long begin, long end) {
195        ContentValues values = new ContentValues();
196        values.put("_id", 1);
197        values.put(CalendarMetaData.LOCAL_TIMEZONE, timezone);
198        values.put(CalendarMetaData.MIN_INSTANCE, begin);
199        values.put(CalendarMetaData.MAX_INSTANCE, end);
200
201        // Atomically update the database and the cached members.
202        try {
203            SQLiteDatabase db = mOpenHelper.getWritableDatabase();
204            db.replace("CalendarMetaData", null, values);
205        } catch (RuntimeException e) {
206            // Failed: zero the in-memory fields to force recomputation.
207            mFields.timezone = null;
208            mFields.minInstance = mFields.maxInstance = 0;
209            throw e;
210        }
211
212        // Update the cached members last in case the database update fails
213        mFields.timezone = timezone;
214        mFields.minInstance = begin;
215        mFields.maxInstance = end;
216    }
217
218    /**
219     * Clears the time range for the Instances table.  The rows in the
220     * Instances table will be deleted (and regenerated) the next time
221     * that the Instances table is queried.
222     *
223     * Also clears the time range for the BusyBits table because that depends
224     * on the Instances table.
225     */
226    public void clearInstanceRange() {
227        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
228        db.beginTransaction();
229        try {
230            // If the fields have not been initialized from the database,
231            // then read the database.
232            if (!mInitialized) {
233                readLocked(db);
234            }
235            writeLocked(mFields.timezone, 0 /* begin */, 0 /* end */);
236            db.setTransactionSuccessful();
237        } finally {
238            db.endTransaction();
239        }
240    }
241}
242