blob: 47d6813725525ebc6c7ff7d453353d578c1d6b9e [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
Hans Boehmf4424772016-12-05 10:35:16 -080017// We make some strong assumptions about the databases we manipulate.
18// We maintain a single table containg expressions, their indices in the sequence of
19// expressions, and some data associated with each expression.
20// All indices are used, except for a small gap around zero. New rows are added
21// either just below the current minimum (negative) index, or just above the current
22// maximum index. Currently no rows are deleted unless we clear the whole table.
23
Hans Boehm8f051c32016-10-03 16:53:58 -070024// TODO: Especially if we notice serious performance issues on rotation in the history
25// view, we may need to use a CursorLoader or some other scheme to preserve the database
26// across rotations.
Hans Boehmf4424772016-12-05 10:35:16 -080027// TODO: We may want to switch to a scheme in which all expressions saved in the database have
28// a positive index, and a flag indicates whether the expression is displayed as part of
29// the history or not. That would avoid potential thrashing between CursorWindows when accessing
30// with a negative index. It would also make it easy to sort expressions in dependency order,
31// which helps with avoiding deep recursion during evaluation. But it makes the history UI
32// implementation more complicated. It should be possible to make this change without a
33// database version bump.
34
35// This ensures strong thread-safety, i.e. each call looks atomic to other threads. We need some
36// such property, since expressions may be read by one thread while the main thread is updating
37// another expression.
Hans Boehm8f051c32016-10-03 16:53:58 -070038
39package com.android.calculator2;
40
Hans Boehmf4424772016-12-05 10:35:16 -080041import android.app.Activity;
Hans Boehm8f051c32016-10-03 16:53:58 -070042import android.content.ContentValues;
43import android.content.Context;
Hans Boehmf4424772016-12-05 10:35:16 -080044import android.database.AbstractWindowedCursor;
Hans Boehm8f051c32016-10-03 16:53:58 -070045import android.database.Cursor;
Hans Boehmf4424772016-12-05 10:35:16 -080046import android.database.CursorWindow;
Hans Boehm8f051c32016-10-03 16:53:58 -070047import android.database.sqlite.SQLiteDatabase;
48import android.database.sqlite.SQLiteException;
49import android.database.sqlite.SQLiteOpenHelper;
50import android.os.AsyncTask;
51import android.provider.BaseColumns;
52import android.util.Log;
Hans Boehmf4424772016-12-05 10:35:16 -080053import android.view.View;
Hans Boehm8f051c32016-10-03 16:53:58 -070054
Hans Boehm8f051c32016-10-03 16:53:58 -070055public class ExpressionDB {
Hans Boehmf4424772016-12-05 10:35:16 -080056 private final boolean CONTINUE_WITH_BAD_DB = false;
57
Hans Boehm8f051c32016-10-03 16:53:58 -070058 /* Table contents */
59 public static class ExpressionEntry implements BaseColumns {
60 public static final String TABLE_NAME = "expressions";
61 public static final String COLUMN_NAME_EXPRESSION = "expression";
62 public static final String COLUMN_NAME_FLAGS = "flags";
63 // Time stamp as returned by currentTimeMillis().
64 public static final String COLUMN_NAME_TIMESTAMP = "timeStamp";
Hans Boehm8f051c32016-10-03 16:53:58 -070065 }
66
67 /* Data to be written to or read from a row in the table */
68 public static class RowData {
69 private static final int DEGREE_MODE = 2;
70 private static final int LONG_TIMEOUT = 1;
71 public final byte[] mExpression;
72 public final int mFlags;
73 public long mTimeStamp; // 0 ==> this and next field to be filled in when written.
Hans Boehm8f051c32016-10-03 16:53:58 -070074 private static int flagsFromDegreeAndTimeout(Boolean DegreeMode, Boolean LongTimeout) {
75 return (DegreeMode ? DEGREE_MODE : 0) | (LongTimeout ? LONG_TIMEOUT : 0);
76 }
77 private boolean degreeModeFromFlags(int flags) {
78 return (flags & DEGREE_MODE) != 0;
79 }
80 private boolean longTimeoutFromFlags(int flags) {
81 return (flags & LONG_TIMEOUT) != 0;
82 }
83 private static final int MILLIS_IN_15_MINS = 15 * 60 * 1000;
Hans Boehm9db3ee22016-11-18 10:09:47 -080084 private RowData(byte[] expr, int flags, long timeStamp) {
Hans Boehm8f051c32016-10-03 16:53:58 -070085 mExpression = expr;
86 mFlags = flags;
87 mTimeStamp = timeStamp;
Hans Boehm8f051c32016-10-03 16:53:58 -070088 }
89 /**
90 * More client-friendly constructor that hides implementation ugliness.
91 * utcOffset here is uncompressed, in milliseconds.
92 * A zero timestamp will cause it to be automatically filled in.
93 */
Hans Boehm9db3ee22016-11-18 10:09:47 -080094 public RowData(byte[] expr, boolean degreeMode, boolean longTimeout, long timeStamp) {
95 this(expr, flagsFromDegreeAndTimeout(degreeMode, longTimeout), timeStamp);
Hans Boehm8f051c32016-10-03 16:53:58 -070096 }
97 public boolean degreeMode() {
98 return degreeModeFromFlags(mFlags);
99 }
100 public boolean longTimeout() {
101 return longTimeoutFromFlags(mFlags);
102 }
103 /**
Hans Boehm8f051c32016-10-03 16:53:58 -0700104 * Return a ContentValues object representing the current data.
105 */
106 public ContentValues toContentValues() {
107 ContentValues cvs = new ContentValues();
108 cvs.put(ExpressionEntry.COLUMN_NAME_EXPRESSION, mExpression);
109 cvs.put(ExpressionEntry.COLUMN_NAME_FLAGS, mFlags);
110 if (mTimeStamp == 0) {
111 mTimeStamp = System.currentTimeMillis();
Hans Boehm8f051c32016-10-03 16:53:58 -0700112 }
113 cvs.put(ExpressionEntry.COLUMN_NAME_TIMESTAMP, mTimeStamp);
Hans Boehm8f051c32016-10-03 16:53:58 -0700114 return cvs;
115 }
116 }
117
118 private static final String SQL_CREATE_ENTRIES =
Hans Boehmf4424772016-12-05 10:35:16 -0800119 "CREATE TABLE " + ExpressionEntry.TABLE_NAME + " ("
120 + ExpressionEntry._ID + " INTEGER PRIMARY KEY,"
121 + ExpressionEntry.COLUMN_NAME_EXPRESSION + " BLOB,"
122 + ExpressionEntry.COLUMN_NAME_FLAGS + " INTEGER,"
123 + ExpressionEntry.COLUMN_NAME_TIMESTAMP + " INTEGER)";
Hans Boehm9db3ee22016-11-18 10:09:47 -0800124 private static final String SQL_DROP_TABLE =
Hans Boehm8f051c32016-10-03 16:53:58 -0700125 "DROP TABLE IF EXISTS " + ExpressionEntry.TABLE_NAME;
Hans Boehmf4424772016-12-05 10:35:16 -0800126 private static final String SQL_GET_MIN = "SELECT MIN(" + ExpressionEntry._ID
127 + ") FROM " + ExpressionEntry.TABLE_NAME;
128 private static final String SQL_GET_MAX = "SELECT MAX(" + ExpressionEntry._ID
129 + ") FROM " + ExpressionEntry.TABLE_NAME;
130 private static final String SQL_GET_ROW = "SELECT * FROM " + ExpressionEntry.TABLE_NAME
131 + " WHERE " + ExpressionEntry._ID + " = ?";
132 private static final String SQL_GET_ALL = "SELECT * FROM " + ExpressionEntry.TABLE_NAME
133 + " WHERE " + ExpressionEntry._ID + " <= ? AND " +
134 ExpressionEntry._ID + " >= ?" + " ORDER BY " + ExpressionEntry._ID + " DESC ";
Hans Boehm9db3ee22016-11-18 10:09:47 -0800135 // We may eventually need an index by timestamp. We don't use it yet.
136 private static final String SQL_CREATE_TIMESTAMP_INDEX =
Hans Boehmf4424772016-12-05 10:35:16 -0800137 "CREATE INDEX timestamp_index ON " + ExpressionEntry.TABLE_NAME + "("
138 + ExpressionEntry.COLUMN_NAME_TIMESTAMP + ")";
Hans Boehm9db3ee22016-11-18 10:09:47 -0800139 private static final String SQL_DROP_TIMESTAMP_INDEX = "DROP INDEX IF EXISTS timestamp_index";
Hans Boehm8f051c32016-10-03 16:53:58 -0700140
141 private class ExpressionDBHelper extends SQLiteOpenHelper {
142 // If you change the database schema, you must increment the database version.
143 public static final int DATABASE_VERSION = 1;
144 public static final String DATABASE_NAME = "Expressions.db";
145
146 public ExpressionDBHelper(Context context) {
147 super(context, DATABASE_NAME, null, DATABASE_VERSION);
148 }
149 public void onCreate(SQLiteDatabase db) {
150 db.execSQL(SQL_CREATE_ENTRIES);
Hans Boehm9db3ee22016-11-18 10:09:47 -0800151 db.execSQL(SQL_CREATE_TIMESTAMP_INDEX);
Hans Boehm8f051c32016-10-03 16:53:58 -0700152 }
153 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
154 // For now just throw away history on database version upgrade/downgrade.
Hans Boehm9db3ee22016-11-18 10:09:47 -0800155 db.execSQL(SQL_DROP_TIMESTAMP_INDEX);
156 db.execSQL(SQL_DROP_TABLE);
Hans Boehm8f051c32016-10-03 16:53:58 -0700157 onCreate(db);
158 }
159 public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
160 onUpgrade(db, oldVersion, newVersion);
161 }
162 }
163
164 private ExpressionDBHelper mExpressionDBHelper;
165
166 private SQLiteDatabase mExpressionDB; // Constant after initialization.
167
Hans Boehmf4424772016-12-05 10:35:16 -0800168 // Expression indices between mMinAccessible and mMaxAccessible inclusive can be accessed.
169 // We set these to more interesting values if a database access fails.
170 // We punt on writes outside this range. We should never read outside this range.
171 // If higher layers refer to an index outside this range, it will already be cached.
172 // This also somewhat limits the size of the database, but only to an unreasonably
173 // huge value.
174 private long mMinAccessible = -10000000L;
175 private long mMaxAccessible = 10000000L;
Hans Boehm8f051c32016-10-03 16:53:58 -0700176
177 // Never allocate new negative indicees (row ids) >= MAXIMUM_MIN_INDEX.
178 public static final long MAXIMUM_MIN_INDEX = -10;
179
180 // Minimum index value in DB.
181 private long mMinIndex;
182 // Maximum index value in DB.
183 private long mMaxIndex;
Hans Boehm8f051c32016-10-03 16:53:58 -0700184
Hans Boehmf4424772016-12-05 10:35:16 -0800185 // A cursor that refers to the whole table, in reverse order.
186 private AbstractWindowedCursor mAllCursor;
187
188 // Expression index corresponding to a zero absolute offset for mAllCursor.
189 // This is the argument we passed to the query.
190 // We explicitly query only for entries that existed when we started, to avoid
191 // interference from updates as we're running. It's unclear whether or not this matters.
192 private int mAllCursorBase;
193
194 // Database has been opened, mMinIndex and mMaxIndex are correct, mAllCursorBase and
195 // mAllCursor have been set.
196 private boolean mDBInitialized;
197
198 // Gap between negative and positive row ids in the database.
199 // Expressions with index [MAXIMUM_MIN_INDEX .. 0] are not stored.
200 private static final long GAP = -MAXIMUM_MIN_INDEX + 1;
201
202 // mLock protects mExpressionDB, mMinAccessible, and mMaxAccessible, mAllCursor,
203 // mAllCursorBase, mMinIndex, mMaxIndex, and mDBInitialized. We access mExpressionDB without
Hans Boehm8f051c32016-10-03 16:53:58 -0700204 // synchronization after it's known to be initialized. Used to wait for database
Hans Boehmf4424772016-12-05 10:35:16 -0800205 // initialization.
Hans Boehm8f051c32016-10-03 16:53:58 -0700206 private Object mLock = new Object();
207
Hans Boehm83f278e2016-12-19 16:20:03 -0800208 public ExpressionDB(Context context) {
209 mExpressionDBHelper = new ExpressionDBHelper(context);
Hans Boehm8f051c32016-10-03 16:53:58 -0700210 AsyncInitializer initializer = new AsyncInitializer();
Hans Boehmf4424772016-12-05 10:35:16 -0800211 // All calls that create background database accesses are made from the UI thread, and
212 // use a SERIAL_EXECUTOR. Thus they execute in order.
213 initializer.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, mExpressionDBHelper);
Hans Boehm8f051c32016-10-03 16:53:58 -0700214 }
215
Hans Boehmf4424772016-12-05 10:35:16 -0800216 // Is database completely unusable?
217 private boolean isDBBad() {
218 if (!CONTINUE_WITH_BAD_DB) {
219 return false;
220 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700221 synchronized(mLock) {
Hans Boehmf4424772016-12-05 10:35:16 -0800222 return mMinAccessible > mMaxAccessible;
Hans Boehm8f051c32016-10-03 16:53:58 -0700223 }
224 }
225
Hans Boehmf4424772016-12-05 10:35:16 -0800226 // Is the index in the accessible range of the database?
227 private boolean inAccessibleRange(long index) {
228 if (!CONTINUE_WITH_BAD_DB) {
229 return true;
230 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700231 synchronized(mLock) {
Hans Boehmf4424772016-12-05 10:35:16 -0800232 return index >= mMinAccessible && index <= mMaxAccessible;
233 }
234 }
235
236
237 private void setBadDB() {
238 if (!CONTINUE_WITH_BAD_DB) {
239 Log.e("Calculator", "Database access failed");
240 throw new RuntimeException("Database access failed");
241 }
242 displayDatabaseWarning();
243 synchronized(mLock) {
244 mMinAccessible = 1L;
245 mMaxAccessible = -1L;
Hans Boehm8f051c32016-10-03 16:53:58 -0700246 }
247 }
248
249 /**
Hans Boehmf4424772016-12-05 10:35:16 -0800250 * Initialize the database in the background.
Hans Boehm8f051c32016-10-03 16:53:58 -0700251 */
252 private class AsyncInitializer extends AsyncTask<ExpressionDBHelper, Void, SQLiteDatabase> {
253 @Override
254 protected SQLiteDatabase doInBackground(ExpressionDBHelper... helper) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700255 try {
Hans Boehmf4424772016-12-05 10:35:16 -0800256 SQLiteDatabase db = helper[0].getWritableDatabase();
Hans Boehm8f051c32016-10-03 16:53:58 -0700257 synchronized(mLock) {
Hans Boehmf4424772016-12-05 10:35:16 -0800258 mExpressionDB = db;
259 try (Cursor minResult = db.rawQuery(SQL_GET_MIN, null)) {
260 if (!minResult.moveToFirst()) {
261 // Empty database.
262 mMinIndex = MAXIMUM_MIN_INDEX;
263 } else {
264 mMinIndex = Math.min(minResult.getLong(0), MAXIMUM_MIN_INDEX);
265 }
266 }
267 try (Cursor maxResult = db.rawQuery(SQL_GET_MAX, null)) {
268 if (!maxResult.moveToFirst()) {
269 // Empty database.
270 mMaxIndex = 0L;
271 } else {
272 mMaxIndex = Math.max(maxResult.getLong(0), 0L);
273 }
274 }
275 if (mMaxIndex > Integer.MAX_VALUE) {
276 throw new AssertionError("Expression index absurdly large");
277 }
278 mAllCursorBase = (int)mMaxIndex;
279 if (mMaxIndex != 0L || mMinIndex != MAXIMUM_MIN_INDEX) {
280 // Set up a cursor for reading the entire database.
281 String args[] = new String[]
282 { Long.toString(mAllCursorBase), Long.toString(mMinIndex) };
283 mAllCursor = (AbstractWindowedCursor) db.rawQuery(SQL_GET_ALL, args);
284 if (!mAllCursor.moveToFirst()) {
285 setBadDB();
286 return null;
287 }
288 }
289 mDBInitialized = true;
290 // We notify here, since there are unlikely cases in which the UI thread
291 // may be blocked on us, preventing onPostExecute from running.
Hans Boehm8f051c32016-10-03 16:53:58 -0700292 mLock.notifyAll();
293 }
Hans Boehmf4424772016-12-05 10:35:16 -0800294 return db;
Hans Boehm8f051c32016-10-03 16:53:58 -0700295 } catch(SQLiteException e) {
296 Log.e("Calculator", "Database initialization failed.\n", e);
297 synchronized(mLock) {
Hans Boehmf4424772016-12-05 10:35:16 -0800298 setBadDB();
Hans Boehm8f051c32016-10-03 16:53:58 -0700299 mLock.notifyAll();
300 }
301 return null;
302 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700303 }
304
305 @Override
306 protected void onPostExecute(SQLiteDatabase result) {
307 if (result == null) {
Hans Boehmf4424772016-12-05 10:35:16 -0800308 displayDatabaseWarning();
Hans Boehm8f051c32016-10-03 16:53:58 -0700309 } // else doInBackground already set expressionDB.
310 }
311 // On cancellation we do nothing;
312 }
313
Hans Boehmf4424772016-12-05 10:35:16 -0800314 private boolean databaseWarningIssued;
315
Hans Boehm8f051c32016-10-03 16:53:58 -0700316 /**
Hans Boehmf4424772016-12-05 10:35:16 -0800317 * Display a warning message that a database access failed.
318 * Do this only once. TODO: Replace with a real UI message.
Hans Boehm8f051c32016-10-03 16:53:58 -0700319 */
Hans Boehmf4424772016-12-05 10:35:16 -0800320 void displayDatabaseWarning() {
321 if (!databaseWarningIssued) {
322 Log.e("Calculator", "Calculator restarting due to database error");
323 databaseWarningIssued = true;
324 }
325 }
326
327 /**
328 * Wait until the database and mAllCursor, etc. have been initialized.
329 */
330 private void waitForDBInitialized() {
Hans Boehm8f051c32016-10-03 16:53:58 -0700331 synchronized(mLock) {
Hans Boehmf4424772016-12-05 10:35:16 -0800332 // InterruptedExceptions are inconvenient here. Defer.
333 boolean caught = false;
334 while (!mDBInitialized && !isDBBad()) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700335 try {
336 mLock.wait();
337 } catch(InterruptedException e) {
Hans Boehmf4424772016-12-05 10:35:16 -0800338 caught = true;
Hans Boehm8f051c32016-10-03 16:53:58 -0700339 }
340 }
Hans Boehmf4424772016-12-05 10:35:16 -0800341 if (caught) {
342 Thread.currentThread().interrupt();
Hans Boehm8f051c32016-10-03 16:53:58 -0700343 }
344 }
345 }
346
347 /**
Hans Boehmf4424772016-12-05 10:35:16 -0800348 * Erase the entire database. Assumes no other accesses to the database are
349 * currently in progress
350 * These tasks must be executed on a serial executor to avoid reordering writes.
Hans Boehm8f051c32016-10-03 16:53:58 -0700351 */
Hans Boehmf4424772016-12-05 10:35:16 -0800352 private class AsyncEraser extends AsyncTask<Void, Void, Void> {
353 @Override
354 protected Void doInBackground(Void... nothings) {
355 mExpressionDB.execSQL(SQL_DROP_TIMESTAMP_INDEX);
356 mExpressionDB.execSQL(SQL_DROP_TABLE);
357 try {
358 mExpressionDB.execSQL("VACUUM");
359 } catch(Exception e) {
360 Log.v("Calculator", "Database VACUUM failed\n", e);
361 // Should only happen with concurrent execution, which should be impossible.
Hans Boehm8f051c32016-10-03 16:53:58 -0700362 }
Hans Boehmf4424772016-12-05 10:35:16 -0800363 mExpressionDB.execSQL(SQL_CREATE_ENTRIES);
364 mExpressionDB.execSQL(SQL_CREATE_TIMESTAMP_INDEX);
365 return null;
366 }
367 @Override
368 protected void onPostExecute(Void nothing) {
369 synchronized(mLock) {
370 // Reinitialize everything to an empty and fully functional database.
371 mMinAccessible = -10000000L;
372 mMaxAccessible = 10000000L;
373 mMinIndex = MAXIMUM_MIN_INDEX;
374 mMaxIndex = mAllCursorBase = 0;
375 mDBInitialized = true;
376 mLock.notifyAll();
Hans Boehm8f051c32016-10-03 16:53:58 -0700377 }
378 }
Hans Boehmf4424772016-12-05 10:35:16 -0800379 // On cancellation we do nothing;
Hans Boehm8f051c32016-10-03 16:53:58 -0700380 }
381
Hans Boehm9db3ee22016-11-18 10:09:47 -0800382 /**
383 * Erase ALL database entries.
384 * This is currently only safe if expressions that may refer to them are also erased.
385 * Should only be called when concurrent references to the database are impossible.
386 * TODO: Look at ways to more selectively clear the database.
387 */
388 public void eraseAll() {
Hans Boehmf4424772016-12-05 10:35:16 -0800389 waitForDBInitialized();
Hans Boehm9db3ee22016-11-18 10:09:47 -0800390 synchronized(mLock) {
Hans Boehmf4424772016-12-05 10:35:16 -0800391 mDBInitialized = false;
Hans Boehm8f051c32016-10-03 16:53:58 -0700392 }
Hans Boehmf4424772016-12-05 10:35:16 -0800393 AsyncEraser eraser = new AsyncEraser();
394 eraser.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
395 }
396
397 /**
398 * Insert the given row in the database without blocking the UI thread.
399 * These tasks must be executed on a serial executor to avoid reordering writes.
400 */
401 private class AsyncWriter extends AsyncTask<ContentValues, Void, Long> {
402 @Override
403 protected Long doInBackground(ContentValues... cvs) {
404 long index = cvs[0].getAsLong(ExpressionEntry._ID);
405 long result = mExpressionDB.insert(ExpressionEntry.TABLE_NAME, null, cvs[0]);
406 // Return 0 on success, row id on failure.
407 if (result == -1) {
408 return index;
409 } else if (result != index) {
410 throw new AssertionError("Expected row id " + index + ", got " + result);
411 } else {
412 return 0L;
413 }
414 }
415 @Override
416 protected void onPostExecute(Long result) {
417 if (result != 0) {
418 synchronized(mLock) {
419 if (result > 0) {
420 mMaxAccessible = result - 1;
421 } else {
422 mMinAccessible = result + 1;
423 }
424 }
425 displayDatabaseWarning();
426 }
427 }
428 // On cancellation we do nothing;
Hans Boehm8f051c32016-10-03 16:53:58 -0700429 }
430
431 /**
432 * Add a row with index outside existing range.
Hans Boehmf4424772016-12-05 10:35:16 -0800433 * The returned index will be just larger than any existing index unless negative_index is true.
Hans Boehm8f051c32016-10-03 16:53:58 -0700434 * In that case it will be smaller than any existing index and smaller than MAXIMUM_MIN_INDEX.
Hans Boehmf4424772016-12-05 10:35:16 -0800435 * This ensures that prior additions have completed, but does not wait for this insertion
436 * to complete.
Hans Boehm8f051c32016-10-03 16:53:58 -0700437 */
Hans Boehmf4424772016-12-05 10:35:16 -0800438 public long addRow(boolean negativeIndex, RowData data) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700439 long result;
440 long newIndex;
Hans Boehmf4424772016-12-05 10:35:16 -0800441 waitForDBInitialized();
Hans Boehm8f051c32016-10-03 16:53:58 -0700442 synchronized(mLock) {
Hans Boehmf4424772016-12-05 10:35:16 -0800443 if (negativeIndex) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700444 newIndex = mMinIndex - 1;
445 mMinIndex = newIndex;
446 } else {
447 newIndex = mMaxIndex + 1;
448 mMaxIndex = newIndex;
449 }
Hans Boehmf4424772016-12-05 10:35:16 -0800450 if (!inAccessibleRange(newIndex)) {
451 // Just drop it, but go ahead and return a new index to use for the cache.
452 // So long as reads of previously written expressions continue to work,
453 // we should be fine. When the application is restarted, history will revert
454 // to just include values between mMinAccessible and mMaxAccessible.
455 return newIndex;
456 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700457 ContentValues cvs = data.toContentValues();
458 cvs.put(ExpressionEntry._ID, newIndex);
Hans Boehmf4424772016-12-05 10:35:16 -0800459 AsyncWriter awriter = new AsyncWriter();
460 // Ensure that writes are executed in order.
461 awriter.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, cvs);
Hans Boehm8f051c32016-10-03 16:53:58 -0700462 }
Hans Boehmf4424772016-12-05 10:35:16 -0800463 return newIndex;
Hans Boehm8f051c32016-10-03 16:53:58 -0700464 }
465
466 /**
Hans Boehmf4424772016-12-05 10:35:16 -0800467 * Generate a fake database row that's good enough to hopefully prevent crashes,
468 * but bad enough to avoid confusion with real data. In particular, the result
469 * will fail to evaluate.
Hans Boehm8f051c32016-10-03 16:53:58 -0700470 */
Hans Boehmf4424772016-12-05 10:35:16 -0800471 RowData makeBadRow() {
472 CalculatorExpr badExpr = new CalculatorExpr();
473 badExpr.add(R.id.lparen);
474 badExpr.add(R.id.rparen);
475 return new RowData(badExpr.toBytes(), false, false, 0);
476 }
477
478 /**
479 * Retrieve the row with the given index using a direct query.
480 * Such a row must exist.
481 * We assume that the database has been initialized, and the argument has been range checked.
482 */
483 private RowData getRowDirect(long index) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700484 RowData result;
Hans Boehm8f051c32016-10-03 16:53:58 -0700485 String args[] = new String[] { Long.toString(index) };
Hans Boehm03a566a2016-11-30 17:56:08 -0800486 try (Cursor resultC = mExpressionDB.rawQuery(SQL_GET_ROW, args)) {
487 if (!resultC.moveToFirst()) {
Hans Boehmf4424772016-12-05 10:35:16 -0800488 setBadDB();
489 return makeBadRow();
Hans Boehm03a566a2016-11-30 17:56:08 -0800490 } else {
491 result = new RowData(resultC.getBlob(1), resultC.getInt(2) /* flags */,
492 resultC.getLong(3) /* timestamp */);
493 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700494 }
495 return result;
496 }
497
Hans Boehmf4424772016-12-05 10:35:16 -0800498 /**
499 * Retrieve the row at the given offset from mAllCursorBase.
500 * Note the argument is NOT an expression index!
501 * We assume that the database has been initialized, and the argument has been range checked.
502 */
503 private RowData getRowFromCursor(int offset) {
504 RowData result;
505 synchronized(mLock) {
506 if (!mAllCursor.moveToPosition(offset)) {
507 Log.e("Calculator", "Failed to move cursor to position " + offset);
508 setBadDB();
509 return makeBadRow();
510 }
511 return new RowData(mAllCursor.getBlob(1), mAllCursor.getInt(2) /* flags */,
512 mAllCursor.getLong(3) /* timestamp */);
513 }
514 }
515
516 /**
517 * Retrieve the database row at the given index.
518 * We currently assume that we never read data that we added since we initialized the database.
519 * This makes sense, since we cache it anyway. And we should always cache recently added data.
520 */
521 public RowData getRow(long index) {
522 waitForDBInitialized();
523 if (!inAccessibleRange(index)) {
524 // Even if something went wrong opening or writing the database, we should
525 // not see such read requests, unless they correspond to a persistently
526 // saved index, and we can't retrieve that expression.
527 displayDatabaseWarning();
528 return makeBadRow();
529 }
530 int position = mAllCursorBase - (int)index;
531 // We currently assume that the only gap between expression indices is the one around 0.
532 if (index < 0) {
533 position -= GAP;
534 }
535 if (position < 0) {
536 throw new AssertionError("Database access out of range");
537 }
538 if (index < 0) {
539 // Avoid using mAllCursor to read data that's far away from the current position,
540 // since we're likely to have to return to the current position.
541 // This is a heuristic; we don't worry about doing the "wrong" thing in the race case.
542 int endPosition;
543 synchronized(mLock) {
544 CursorWindow window = mAllCursor.getWindow();
545 endPosition = window.getStartPosition() + window.getNumRows();
546 }
547 if (position >= endPosition) {
548 return getRowDirect(index);
549 }
550 }
551 // In the positive index case, it's probably OK to cross a cursor boundary, since
552 // we're much more likely to stay in the new window.
553 return getRowFromCursor(position);
554 }
555
Hans Boehm8f051c32016-10-03 16:53:58 -0700556 public long getMinIndex() {
Hans Boehmf4424772016-12-05 10:35:16 -0800557 waitForDBInitialized();
Hans Boehm8f051c32016-10-03 16:53:58 -0700558 synchronized(mLock) {
559 return mMinIndex;
560 }
561 }
562
563 public long getMaxIndex() {
Hans Boehmf4424772016-12-05 10:35:16 -0800564 waitForDBInitialized();
Hans Boehm8f051c32016-10-03 16:53:58 -0700565 synchronized(mLock) {
566 return mMaxIndex;
567 }
568 }
569
570 public void close() {
571 mExpressionDBHelper.close();
572 }
573
574}