1/*
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// 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
24// 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.
27// 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.
38
39package com.android.calculator2;
40
41import android.app.Activity;
42import android.content.ContentValues;
43import android.content.Context;
44import android.database.AbstractWindowedCursor;
45import android.database.Cursor;
46import android.database.CursorWindow;
47import 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;
53import android.view.View;
54
55public class ExpressionDB {
56    private final boolean CONTINUE_WITH_BAD_DB = false;
57
58    /* 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";
65    }
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.
74        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;
84        private RowData(byte[] expr, int flags, long timeStamp) {
85            mExpression = expr;
86            mFlags = flags;
87            mTimeStamp = timeStamp;
88        }
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         */
94        public RowData(byte[] expr, boolean degreeMode, boolean longTimeout, long timeStamp) {
95            this(expr, flagsFromDegreeAndTimeout(degreeMode, longTimeout), timeStamp);
96        }
97        public boolean degreeMode() {
98            return degreeModeFromFlags(mFlags);
99        }
100        public boolean longTimeout() {
101            return longTimeoutFromFlags(mFlags);
102        }
103        /**
104         * 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();
112            }
113            cvs.put(ExpressionEntry.COLUMN_NAME_TIMESTAMP, mTimeStamp);
114            return cvs;
115        }
116    }
117
118    private static final String SQL_CREATE_ENTRIES =
119            "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)";
124    private static final String SQL_DROP_TABLE =
125            "DROP TABLE IF EXISTS " + ExpressionEntry.TABLE_NAME;
126    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 ";
135    // We may eventually need an index by timestamp. We don't use it yet.
136    private static final String SQL_CREATE_TIMESTAMP_INDEX =
137            "CREATE INDEX timestamp_index ON " + ExpressionEntry.TABLE_NAME + "("
138            + ExpressionEntry.COLUMN_NAME_TIMESTAMP + ")";
139    private static final String SQL_DROP_TIMESTAMP_INDEX = "DROP INDEX IF EXISTS timestamp_index";
140
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);
151            db.execSQL(SQL_CREATE_TIMESTAMP_INDEX);
152        }
153        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
154            // For now just throw away history on database version upgrade/downgrade.
155            db.execSQL(SQL_DROP_TIMESTAMP_INDEX);
156            db.execSQL(SQL_DROP_TABLE);
157            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
168    // 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;
176
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;
184
185    // 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
204    // synchronization after it's known to be initialized.  Used to wait for database
205    // initialization.
206    private Object mLock = new Object();
207
208    public ExpressionDB(Context context) {
209        mExpressionDBHelper = new ExpressionDBHelper(context);
210        AsyncInitializer initializer = new AsyncInitializer();
211        // 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);
214    }
215
216    // Is database completely unusable?
217    private boolean isDBBad() {
218        if (!CONTINUE_WITH_BAD_DB) {
219            return false;
220        }
221        synchronized(mLock) {
222            return mMinAccessible > mMaxAccessible;
223        }
224    }
225
226    // 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        }
231        synchronized(mLock) {
232            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;
246        }
247    }
248
249    /**
250     * Initialize the database in the background.
251     */
252    private class AsyncInitializer extends AsyncTask<ExpressionDBHelper, Void, SQLiteDatabase> {
253        @Override
254        protected SQLiteDatabase doInBackground(ExpressionDBHelper... helper) {
255            try {
256                SQLiteDatabase db = helper[0].getWritableDatabase();
257                synchronized(mLock) {
258                    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.
292                    mLock.notifyAll();
293                }
294                return db;
295            } catch(SQLiteException e) {
296                Log.e("Calculator", "Database initialization failed.\n", e);
297                synchronized(mLock) {
298                    setBadDB();
299                    mLock.notifyAll();
300                }
301                return null;
302            }
303        }
304
305        @Override
306        protected void onPostExecute(SQLiteDatabase result) {
307            if (result == null) {
308                displayDatabaseWarning();
309            } // else doInBackground already set expressionDB.
310        }
311        // On cancellation we do nothing;
312    }
313
314    private boolean databaseWarningIssued;
315
316    /**
317     * Display a warning message that a database access failed.
318     * Do this only once. TODO: Replace with a real UI message.
319     */
320    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() {
331        synchronized(mLock) {
332            // InterruptedExceptions are inconvenient here. Defer.
333            boolean caught = false;
334            while (!mDBInitialized && !isDBBad()) {
335                try {
336                    mLock.wait();
337                } catch(InterruptedException e) {
338                    caught = true;
339                }
340            }
341            if (caught) {
342                Thread.currentThread().interrupt();
343            }
344        }
345    }
346
347    /**
348     * 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.
351     */
352    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.
362            }
363            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();
377            }
378        }
379        // On cancellation we do nothing;
380    }
381
382    /**
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() {
389        waitForDBInitialized();
390        synchronized(mLock) {
391            mDBInitialized = false;
392        }
393        AsyncEraser eraser = new AsyncEraser();
394        eraser.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
395    }
396
397    // 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
439    /**
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]);
448            writeCompleted();
449            // 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;
472    }
473
474    /**
475     * Add a row with index outside existing range.
476     * The returned index will be just larger than any existing index unless negative_index is true.
477     * In that case it will be smaller than any existing index and smaller than MAXIMUM_MIN_INDEX.
478     * This ensures that prior additions have completed, but does not wait for this insertion
479     * to complete.
480     */
481    public long addRow(boolean negativeIndex, RowData data) {
482        long result;
483        long newIndex;
484        waitForDBInitialized();
485        synchronized(mLock) {
486            if (negativeIndex) {
487                newIndex = mMinIndex - 1;
488                mMinIndex = newIndex;
489            } else {
490                newIndex = mMaxIndex + 1;
491                mMaxIndex = newIndex;
492            }
493            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            }
500            writeStarted();
501            ContentValues cvs = data.toContentValues();
502            cvs.put(ExpressionEntry._ID, newIndex);
503            AsyncWriter awriter = new AsyncWriter();
504            // Ensure that writes are executed in order.
505            awriter.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, cvs);
506        }
507        return newIndex;
508    }
509
510    /**
511     * 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.
514     */
515    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) {
528        RowData result;
529        String args[] = new String[] { Long.toString(index) };
530        try (Cursor resultC = mExpressionDB.rawQuery(SQL_GET_ROW, args)) {
531            if (!resultC.moveToFirst()) {
532                setBadDB();
533                return makeBadRow();
534            } else {
535                result = new RowData(resultC.getBlob(1), resultC.getInt(2) /* flags */,
536                        resultC.getLong(3) /* timestamp */);
537            }
538        }
539        return result;
540    }
541
542    /**
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) {
580            throw new AssertionError("Database access out of range, index = " + index
581                    + " rel. pos. = " + position);
582        }
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
601    public long getMinIndex() {
602        waitForDBInitialized();
603        synchronized(mLock) {
604            return mMinIndex;
605        }
606    }
607
608    public long getMaxIndex() {
609        waitForDBInitialized();
610        synchronized(mLock) {
611            return mMaxIndex;
612        }
613    }
614
615    public void close() {
616        mExpressionDBHelper.close();
617    }
618
619}
620