blob: d3091a9e6d66e3b5786baa96d4c0043bf35c0410 [file] [log] [blame]
Hans Boehm8f051c32016-10-03 16:53:58 -07001/*
2 * Copyright (C) 2016 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// FIXME: We need to rethink the error handling here. Do we want to revert to history-less
18// operation if something goes wrong with the database?
19// TODO: This tries to ensure strong thread-safety, i.e. each call looks atomic, both to
20// other threads and other database users. Is this useful?
21// TODO: Especially if we notice serious performance issues on rotation in the history
22// view, we may need to use a CursorLoader or some other scheme to preserve the database
23// across rotations.
24
25package com.android.calculator2;
26
27import android.content.ContentValues;
28import android.content.Context;
29import android.database.Cursor;
30import android.database.sqlite.SQLiteDatabase;
31import android.database.sqlite.SQLiteException;
32import android.database.sqlite.SQLiteOpenHelper;
33import android.os.AsyncTask;
34import android.provider.BaseColumns;
35import android.util.Log;
36
Hans Boehm8f051c32016-10-03 16:53:58 -070037public class ExpressionDB {
38 /* Table contents */
39 public static class ExpressionEntry implements BaseColumns {
40 public static final String TABLE_NAME = "expressions";
41 public static final String COLUMN_NAME_EXPRESSION = "expression";
42 public static final String COLUMN_NAME_FLAGS = "flags";
43 // Time stamp as returned by currentTimeMillis().
44 public static final String COLUMN_NAME_TIMESTAMP = "timeStamp";
Hans Boehm8f051c32016-10-03 16:53:58 -070045 }
46
47 /* Data to be written to or read from a row in the table */
48 public static class RowData {
49 private static final int DEGREE_MODE = 2;
50 private static final int LONG_TIMEOUT = 1;
51 public final byte[] mExpression;
52 public final int mFlags;
53 public long mTimeStamp; // 0 ==> this and next field to be filled in when written.
Hans Boehm8f051c32016-10-03 16:53:58 -070054 private static int flagsFromDegreeAndTimeout(Boolean DegreeMode, Boolean LongTimeout) {
55 return (DegreeMode ? DEGREE_MODE : 0) | (LongTimeout ? LONG_TIMEOUT : 0);
56 }
57 private boolean degreeModeFromFlags(int flags) {
58 return (flags & DEGREE_MODE) != 0;
59 }
60 private boolean longTimeoutFromFlags(int flags) {
61 return (flags & LONG_TIMEOUT) != 0;
62 }
63 private static final int MILLIS_IN_15_MINS = 15 * 60 * 1000;
Hans Boehm9db3ee22016-11-18 10:09:47 -080064 private RowData(byte[] expr, int flags, long timeStamp) {
Hans Boehm8f051c32016-10-03 16:53:58 -070065 mExpression = expr;
66 mFlags = flags;
67 mTimeStamp = timeStamp;
Hans Boehm8f051c32016-10-03 16:53:58 -070068 }
69 /**
70 * More client-friendly constructor that hides implementation ugliness.
71 * utcOffset here is uncompressed, in milliseconds.
72 * A zero timestamp will cause it to be automatically filled in.
73 */
Hans Boehm9db3ee22016-11-18 10:09:47 -080074 public RowData(byte[] expr, boolean degreeMode, boolean longTimeout, long timeStamp) {
75 this(expr, flagsFromDegreeAndTimeout(degreeMode, longTimeout), timeStamp);
Hans Boehm8f051c32016-10-03 16:53:58 -070076 }
77 public boolean degreeMode() {
78 return degreeModeFromFlags(mFlags);
79 }
80 public boolean longTimeout() {
81 return longTimeoutFromFlags(mFlags);
82 }
83 /**
Hans Boehm8f051c32016-10-03 16:53:58 -070084 * Return a ContentValues object representing the current data.
85 */
86 public ContentValues toContentValues() {
87 ContentValues cvs = new ContentValues();
88 cvs.put(ExpressionEntry.COLUMN_NAME_EXPRESSION, mExpression);
89 cvs.put(ExpressionEntry.COLUMN_NAME_FLAGS, mFlags);
90 if (mTimeStamp == 0) {
91 mTimeStamp = System.currentTimeMillis();
Hans Boehm8f051c32016-10-03 16:53:58 -070092 }
93 cvs.put(ExpressionEntry.COLUMN_NAME_TIMESTAMP, mTimeStamp);
Hans Boehm8f051c32016-10-03 16:53:58 -070094 return cvs;
95 }
96 }
97
98 private static final String SQL_CREATE_ENTRIES =
99 "CREATE TABLE " + ExpressionEntry.TABLE_NAME + " (" +
100 ExpressionEntry._ID + " INTEGER PRIMARY KEY," +
101 ExpressionEntry.COLUMN_NAME_EXPRESSION + " BLOB," +
102 ExpressionEntry.COLUMN_NAME_FLAGS + " INTEGER," +
Hans Boehm9db3ee22016-11-18 10:09:47 -0800103 ExpressionEntry.COLUMN_NAME_TIMESTAMP + " INTEGER)";
104 private static final String SQL_DROP_TABLE =
Hans Boehm8f051c32016-10-03 16:53:58 -0700105 "DROP TABLE IF EXISTS " + ExpressionEntry.TABLE_NAME;
106 private static final String SQL_GET_MIN = "SELECT MIN(" + ExpressionEntry._ID +
107 ") FROM " + ExpressionEntry.TABLE_NAME;
108 private static final String SQL_GET_MAX = "SELECT MAX(" + ExpressionEntry._ID +
109 ") FROM " + ExpressionEntry.TABLE_NAME;
110 private static final String SQL_GET_ROW = "SELECT * FROM " + ExpressionEntry.TABLE_NAME +
111 " WHERE " + ExpressionEntry._ID + " = ?";
Hans Boehm9db3ee22016-11-18 10:09:47 -0800112 // We may eventually need an index by timestamp. We don't use it yet.
113 private static final String SQL_CREATE_TIMESTAMP_INDEX =
114 "CREATE INDEX timestamp_index ON " + ExpressionEntry.TABLE_NAME + "(" +
115 ExpressionEntry.COLUMN_NAME_TIMESTAMP + ")";
116 private static final String SQL_DROP_TIMESTAMP_INDEX = "DROP INDEX IF EXISTS timestamp_index";
Hans Boehm8f051c32016-10-03 16:53:58 -0700117
118 private class ExpressionDBHelper extends SQLiteOpenHelper {
119 // If you change the database schema, you must increment the database version.
120 public static final int DATABASE_VERSION = 1;
121 public static final String DATABASE_NAME = "Expressions.db";
122
123 public ExpressionDBHelper(Context context) {
124 super(context, DATABASE_NAME, null, DATABASE_VERSION);
125 }
126 public void onCreate(SQLiteDatabase db) {
127 db.execSQL(SQL_CREATE_ENTRIES);
Hans Boehm9db3ee22016-11-18 10:09:47 -0800128 db.execSQL(SQL_CREATE_TIMESTAMP_INDEX);
Hans Boehm8f051c32016-10-03 16:53:58 -0700129 }
130 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
131 // For now just throw away history on database version upgrade/downgrade.
Hans Boehm9db3ee22016-11-18 10:09:47 -0800132 db.execSQL(SQL_DROP_TIMESTAMP_INDEX);
133 db.execSQL(SQL_DROP_TABLE);
Hans Boehm8f051c32016-10-03 16:53:58 -0700134 onCreate(db);
135 }
136 public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
137 onUpgrade(db, oldVersion, newVersion);
138 }
139 }
140
141 private ExpressionDBHelper mExpressionDBHelper;
142
143 private SQLiteDatabase mExpressionDB; // Constant after initialization.
144
145 private boolean mBadDB = false; // Database initialization failed.
146
147 // Never allocate new negative indicees (row ids) >= MAXIMUM_MIN_INDEX.
148 public static final long MAXIMUM_MIN_INDEX = -10;
149
150 // Minimum index value in DB.
151 private long mMinIndex;
152 // Maximum index value in DB.
153 private long mMaxIndex;
154 // mMinIndex and mMaxIndex are correct.
155 private boolean mMinMaxValid;
156
157 // mLock protects mExpressionDB and mBadDB, though we access mExpressionDB without
158 // synchronization after it's known to be initialized. Used to wait for database
159 // initialization. Also protects mMinIndex, mMaxIndex, and mMinMaxValid.
160 private Object mLock = new Object();
161
162 public ExpressionDB(Context context) {
163 mExpressionDBHelper = new ExpressionDBHelper(context);
164 AsyncInitializer initializer = new AsyncInitializer();
165 initializer.execute(mExpressionDBHelper);
166 }
167
168 private boolean getBadDB() {
169 synchronized(mLock) {
170 return mBadDB;
171 }
172 }
173
174 private void setBadDB() {
175 synchronized(mLock) {
176 mBadDB = true;
177 }
178 }
179
180 /**
181 * Set mExpressionDB and compute minimum and maximum indices in the background.
182 */
183 private class AsyncInitializer extends AsyncTask<ExpressionDBHelper, Void, SQLiteDatabase> {
184 @Override
185 protected SQLiteDatabase doInBackground(ExpressionDBHelper... helper) {
186 SQLiteDatabase result;
187 try {
188 result = helper[0].getWritableDatabase();
189 // We notify here, since there are unlikely cases in which the UI thread
190 // may be blocked on us, preventing onPostExecute from running.
191 synchronized(mLock) {
192 mExpressionDB = result;
193 mLock.notifyAll();
194 }
195 long min, max;
196 try (Cursor minResult = result.rawQuery(SQL_GET_MIN, null)) {
197 if (!minResult.moveToFirst()) {
198 // Empty database.
199 min = MAXIMUM_MIN_INDEX;
200 } else {
201 min = Math.min(minResult.getLong(0), MAXIMUM_MIN_INDEX);
202 }
203 }
204 try (Cursor maxResult = result.rawQuery(SQL_GET_MAX, null)) {
205 if (!maxResult.moveToFirst()) {
206 // Empty database.
207 max = 0L;
208 } else {
209 max = Math.max(maxResult.getLong(0), 0L);
210 }
211 }
212 synchronized(mLock) {
213 mMinIndex = min;
214 mMaxIndex = max;
215 mMinMaxValid = true;
216 mLock.notifyAll();
217 }
218 } catch(SQLiteException e) {
219 Log.e("Calculator", "Database initialization failed.\n", e);
220 synchronized(mLock) {
221 mBadDB = true;
222 mLock.notifyAll();
223 }
224 return null;
225 }
226 return result;
227 }
228
229 @Override
230 protected void onPostExecute(SQLiteDatabase result) {
231 if (result == null) {
232 throw new AssertionError("Failed to open history DB");
233 // TODO: Should we try to run without persistent history instead?
234 } // else doInBackground already set expressionDB.
235 }
236 // On cancellation we do nothing;
237 }
238
239 /**
240 * Wait until expression DB is ready.
241 * This should usually be a no-op, since we set up the DB on creation. But there are a few
242 * cases, such as restarting the calculator in history mode, when we currently can't do
243 * anything but wait, possibly even in the UI thread.
244 */
245 private void waitForExpressionDB() {
246 synchronized(mLock) {
247 while (mExpressionDB == null && !mBadDB) {
248 try {
249 mLock.wait();
250 } catch(InterruptedException e) {
251 mBadDB = true;
252 }
253 }
254 if (mBadDB) {
255 throw new AssertionError("Failed to open history DB");
256 }
257 }
258 }
259
260 /**
261 * Wait until the minimum key has been computed.
262 */
263 private void waitForMinMaxValid() {
264 synchronized(mLock) {
265 while (!mMinMaxValid && !mBadDB) {
266 try {
267 mLock.wait();
268 } catch(InterruptedException e) {
269 mBadDB = true;
270 }
271 }
272 if (mBadDB) {
273 throw new AssertionError("Failed to compute minimum key");
274 }
275 }
276 }
277
Hans Boehm9db3ee22016-11-18 10:09:47 -0800278 /**
279 * Erase ALL database entries.
280 * This is currently only safe if expressions that may refer to them are also erased.
281 * Should only be called when concurrent references to the database are impossible.
282 * TODO: Look at ways to more selectively clear the database.
283 */
284 public void eraseAll() {
Hans Boehm8f051c32016-10-03 16:53:58 -0700285 waitForExpressionDB();
Hans Boehm9db3ee22016-11-18 10:09:47 -0800286 mExpressionDB.execSQL(SQL_DROP_TIMESTAMP_INDEX);
287 mExpressionDB.execSQL(SQL_DROP_TABLE);
Hans Boehm8f051c32016-10-03 16:53:58 -0700288 try {
289 mExpressionDB.execSQL("VACUUM");
290 } catch(Exception e) {
291 Log.v("Calculator", "Database VACUUM failed\n", e);
Hans Boehm9db3ee22016-11-18 10:09:47 -0800292 // Should only happen with concurrent execution, which should be impossible.
Hans Boehm8f051c32016-10-03 16:53:58 -0700293 }
Hans Boehm9db3ee22016-11-18 10:09:47 -0800294 mExpressionDB.execSQL(SQL_CREATE_ENTRIES);
295 mExpressionDB.execSQL(SQL_CREATE_TIMESTAMP_INDEX);
296 synchronized(mLock) {
297 mMinIndex = MAXIMUM_MIN_INDEX;
298 mMaxIndex = 0L;
Hans Boehm8f051c32016-10-03 16:53:58 -0700299 }
300 }
301
302 /**
303 * Add a row with index outside existing range.
304 * The returned index will be larger than any existing index unless negative_index is true.
305 * In that case it will be smaller than any existing index and smaller than MAXIMUM_MIN_INDEX.
306 */
307 public long addRow(boolean negative_index, RowData data) {
308 long result;
309 long newIndex;
310 waitForMinMaxValid();
311 synchronized(mLock) {
312 if (negative_index) {
313 newIndex = mMinIndex - 1;
314 mMinIndex = newIndex;
315 } else {
316 newIndex = mMaxIndex + 1;
317 mMaxIndex = newIndex;
318 }
319 ContentValues cvs = data.toContentValues();
320 cvs.put(ExpressionEntry._ID, newIndex);
321 result = mExpressionDB.insert(ExpressionEntry.TABLE_NAME, null, cvs);
322 }
323 if (result != newIndex) {
324 throw new AssertionError("Expected row id " + newIndex + ", got " + result);
325 }
326 return result;
327 }
328
329 /**
330 * Retrieve the row with the given index.
331 * Such a row must exist.
332 */
333 public RowData getRow(long index) {
334 RowData result;
335 waitForExpressionDB();
336 String args[] = new String[] { Long.toString(index) };
Hans Boehm03a566a2016-11-30 17:56:08 -0800337 try (Cursor resultC = mExpressionDB.rawQuery(SQL_GET_ROW, args)) {
338 if (!resultC.moveToFirst()) {
339 throw new AssertionError("Missing Row");
340 } else {
341 result = new RowData(resultC.getBlob(1), resultC.getInt(2) /* flags */,
342 resultC.getLong(3) /* timestamp */);
343 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700344 }
345 return result;
346 }
347
348 public long getMinIndex() {
349 waitForMinMaxValid();
350 synchronized(mLock) {
351 return mMinIndex;
352 }
353 }
354
355 public long getMaxIndex() {
356 waitForMinMaxValid();
357 synchronized(mLock) {
358 return mMaxIndex;
359 }
360 }
361
362 public void close() {
363 mExpressionDBHelper.close();
364 }
365
366}