blob: 9a0f8ecb17f800debd05a12cb8e3baa53adacd4b [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
Hans Boehme95203e2017-01-04 14:13:11 -0800397 // We track the number of outstanding writes to prevent onSaveInstanceState from
398 // completing with in-flight database writes.
399
400 private int mIncompleteWrites = 0;
401 private Object mWriteCountsLock = new Object(); // Protects the preceding field.
402
403 private void writeCompleted() {
404 synchronized(mWriteCountsLock) {
405 if (--mIncompleteWrites == 0) {
406 mWriteCountsLock.notifyAll();
407 }
408 }
409 }
410
411 private void writeStarted() {
412 synchronized(mWriteCountsLock) {
413 ++mIncompleteWrites;
414 }
415 }
416
417 /**
418 * Wait for in-flight writes to complete.
419 * This is not safe to call from one of our background tasks, since the writing
420 * tasks may be waiting for the same underlying thread that we're using, resulting
421 * in deadlock.
422 */
423 public void waitForWrites() {
424 synchronized(mWriteCountsLock) {
425 boolean caught = false;
426 while (mIncompleteWrites != 0) {
427 try {
428 mWriteCountsLock.wait();
429 } catch (InterruptedException e) {
430 caught = true;
431 }
432 }
433 if (caught) {
434 Thread.currentThread().interrupt();
435 }
436 }
437 }
438
Hans Boehmf4424772016-12-05 10:35:16 -0800439 /**
440 * Insert the given row in the database without blocking the UI thread.
441 * These tasks must be executed on a serial executor to avoid reordering writes.
442 */
443 private class AsyncWriter extends AsyncTask<ContentValues, Void, Long> {
444 @Override
445 protected Long doInBackground(ContentValues... cvs) {
446 long index = cvs[0].getAsLong(ExpressionEntry._ID);
447 long result = mExpressionDB.insert(ExpressionEntry.TABLE_NAME, null, cvs[0]);
Hans Boehme95203e2017-01-04 14:13:11 -0800448 writeCompleted();
Hans Boehmf4424772016-12-05 10:35:16 -0800449 // Return 0 on success, row id on failure.
450 if (result == -1) {
451 return index;
452 } else if (result != index) {
453 throw new AssertionError("Expected row id " + index + ", got " + result);
454 } else {
455 return 0L;
456 }
457 }
458 @Override
459 protected void onPostExecute(Long result) {
460 if (result != 0) {
461 synchronized(mLock) {
462 if (result > 0) {
463 mMaxAccessible = result - 1;
464 } else {
465 mMinAccessible = result + 1;
466 }
467 }
468 displayDatabaseWarning();
469 }
470 }
471 // On cancellation we do nothing;
Hans Boehm8f051c32016-10-03 16:53:58 -0700472 }
473
474 /**
475 * Add a row with index outside existing range.
Hans Boehmf4424772016-12-05 10:35:16 -0800476 * The returned index will be just larger than any existing index unless negative_index is true.
Hans Boehm8f051c32016-10-03 16:53:58 -0700477 * In that case it will be smaller than any existing index and smaller than MAXIMUM_MIN_INDEX.
Hans Boehmf4424772016-12-05 10:35:16 -0800478 * This ensures that prior additions have completed, but does not wait for this insertion
479 * to complete.
Hans Boehm8f051c32016-10-03 16:53:58 -0700480 */
Hans Boehmf4424772016-12-05 10:35:16 -0800481 public long addRow(boolean negativeIndex, RowData data) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700482 long result;
483 long newIndex;
Hans Boehmf4424772016-12-05 10:35:16 -0800484 waitForDBInitialized();
Hans Boehm8f051c32016-10-03 16:53:58 -0700485 synchronized(mLock) {
Hans Boehmf4424772016-12-05 10:35:16 -0800486 if (negativeIndex) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700487 newIndex = mMinIndex - 1;
488 mMinIndex = newIndex;
489 } else {
490 newIndex = mMaxIndex + 1;
491 mMaxIndex = newIndex;
492 }
Hans Boehmf4424772016-12-05 10:35:16 -0800493 if (!inAccessibleRange(newIndex)) {
494 // Just drop it, but go ahead and return a new index to use for the cache.
495 // So long as reads of previously written expressions continue to work,
496 // we should be fine. When the application is restarted, history will revert
497 // to just include values between mMinAccessible and mMaxAccessible.
498 return newIndex;
499 }
Hans Boehme95203e2017-01-04 14:13:11 -0800500 writeStarted();
Hans Boehm8f051c32016-10-03 16:53:58 -0700501 ContentValues cvs = data.toContentValues();
502 cvs.put(ExpressionEntry._ID, newIndex);
Hans Boehmf4424772016-12-05 10:35:16 -0800503 AsyncWriter awriter = new AsyncWriter();
504 // Ensure that writes are executed in order.
505 awriter.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, cvs);
Hans Boehm8f051c32016-10-03 16:53:58 -0700506 }
Hans Boehmf4424772016-12-05 10:35:16 -0800507 return newIndex;
Hans Boehm8f051c32016-10-03 16:53:58 -0700508 }
509
510 /**
Hans Boehmf4424772016-12-05 10:35:16 -0800511 * Generate a fake database row that's good enough to hopefully prevent crashes,
512 * but bad enough to avoid confusion with real data. In particular, the result
513 * will fail to evaluate.
Hans Boehm8f051c32016-10-03 16:53:58 -0700514 */
Hans Boehmf4424772016-12-05 10:35:16 -0800515 RowData makeBadRow() {
516 CalculatorExpr badExpr = new CalculatorExpr();
517 badExpr.add(R.id.lparen);
518 badExpr.add(R.id.rparen);
519 return new RowData(badExpr.toBytes(), false, false, 0);
520 }
521
522 /**
523 * Retrieve the row with the given index using a direct query.
524 * Such a row must exist.
525 * We assume that the database has been initialized, and the argument has been range checked.
526 */
527 private RowData getRowDirect(long index) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700528 RowData result;
Hans Boehm8f051c32016-10-03 16:53:58 -0700529 String args[] = new String[] { Long.toString(index) };
Hans Boehm03a566a2016-11-30 17:56:08 -0800530 try (Cursor resultC = mExpressionDB.rawQuery(SQL_GET_ROW, args)) {
531 if (!resultC.moveToFirst()) {
Hans Boehmf4424772016-12-05 10:35:16 -0800532 setBadDB();
533 return makeBadRow();
Hans Boehm03a566a2016-11-30 17:56:08 -0800534 } else {
535 result = new RowData(resultC.getBlob(1), resultC.getInt(2) /* flags */,
536 resultC.getLong(3) /* timestamp */);
537 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700538 }
539 return result;
540 }
541
Hans Boehmf4424772016-12-05 10:35:16 -0800542 /**
543 * Retrieve the row at the given offset from mAllCursorBase.
544 * Note the argument is NOT an expression index!
545 * We assume that the database has been initialized, and the argument has been range checked.
546 */
547 private RowData getRowFromCursor(int offset) {
548 RowData result;
549 synchronized(mLock) {
550 if (!mAllCursor.moveToPosition(offset)) {
551 Log.e("Calculator", "Failed to move cursor to position " + offset);
552 setBadDB();
553 return makeBadRow();
554 }
555 return new RowData(mAllCursor.getBlob(1), mAllCursor.getInt(2) /* flags */,
556 mAllCursor.getLong(3) /* timestamp */);
557 }
558 }
559
560 /**
561 * Retrieve the database row at the given index.
562 * We currently assume that we never read data that we added since we initialized the database.
563 * This makes sense, since we cache it anyway. And we should always cache recently added data.
564 */
565 public RowData getRow(long index) {
566 waitForDBInitialized();
567 if (!inAccessibleRange(index)) {
568 // Even if something went wrong opening or writing the database, we should
569 // not see such read requests, unless they correspond to a persistently
570 // saved index, and we can't retrieve that expression.
571 displayDatabaseWarning();
572 return makeBadRow();
573 }
574 int position = mAllCursorBase - (int)index;
575 // We currently assume that the only gap between expression indices is the one around 0.
576 if (index < 0) {
577 position -= GAP;
578 }
579 if (position < 0) {
Hans Boehm5e139652017-01-03 14:10:17 -0800580 throw new AssertionError("Database access out of range, index = " + index
581 + " rel. pos. = " + position);
Hans Boehmf4424772016-12-05 10:35:16 -0800582 }
583 if (index < 0) {
584 // Avoid using mAllCursor to read data that's far away from the current position,
585 // since we're likely to have to return to the current position.
586 // This is a heuristic; we don't worry about doing the "wrong" thing in the race case.
587 int endPosition;
588 synchronized(mLock) {
589 CursorWindow window = mAllCursor.getWindow();
590 endPosition = window.getStartPosition() + window.getNumRows();
591 }
592 if (position >= endPosition) {
593 return getRowDirect(index);
594 }
595 }
596 // In the positive index case, it's probably OK to cross a cursor boundary, since
597 // we're much more likely to stay in the new window.
598 return getRowFromCursor(position);
599 }
600
Hans Boehm8f051c32016-10-03 16:53:58 -0700601 public long getMinIndex() {
Hans Boehmf4424772016-12-05 10:35:16 -0800602 waitForDBInitialized();
Hans Boehm8f051c32016-10-03 16:53:58 -0700603 synchronized(mLock) {
604 return mMinIndex;
605 }
606 }
607
608 public long getMaxIndex() {
Hans Boehmf4424772016-12-05 10:35:16 -0800609 waitForDBInitialized();
Hans Boehm8f051c32016-10-03 16:53:58 -0700610 synchronized(mLock) {
611 return mMaxIndex;
612 }
613 }
614
615 public void close() {
616 mExpressionDBHelper.close();
617 }
618
619}