summaryrefslogtreecommitdiff
path: root/src/com/android/calculator2/ExpressionDB.java
blob: 9a0f8ecb17f800debd05a12cb8e3baa53adacd4b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// We make some strong assumptions about the databases we manipulate.
// We maintain a single table containg expressions, their indices in the sequence of
// expressions, and some data associated with each expression.
// All indices are used, except for a small gap around zero.  New rows are added
// either just below the current minimum (negative) index, or just above the current
// maximum index. Currently no rows are deleted unless we clear the whole table.

// TODO: Especially if we notice serious performance issues on rotation in the history
// view, we may need to use a CursorLoader or some other scheme to preserve the database
// across rotations.
// TODO: We may want to switch to a scheme in which all expressions saved in the database have
// a positive index, and a flag indicates whether the expression is displayed as part of
// the history or not. That would avoid potential thrashing between CursorWindows when accessing
// with a negative index. It would also make it easy to sort expressions in dependency order,
// which helps with avoiding deep recursion during evaluation. But it makes the history UI
// implementation more complicated. It should be possible to make this change without a
// database version bump.

// This ensures strong thread-safety, i.e. each call looks atomic to other threads. We need some
// such property, since expressions may be read by one thread while the main thread is updating
// another expression.

package com.android.calculator2;

import android.app.Activity;
import android.content.ContentValues;
import android.content.Context;
import android.database.AbstractWindowedCursor;
import android.database.Cursor;
import android.database.CursorWindow;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.AsyncTask;
import android.provider.BaseColumns;
import android.util.Log;
import android.view.View;

public class ExpressionDB {
    private final boolean CONTINUE_WITH_BAD_DB = false;

    /* Table contents */
    public static class ExpressionEntry implements BaseColumns {
        public static final String TABLE_NAME = "expressions";
        public static final String COLUMN_NAME_EXPRESSION = "expression";
        public static final String COLUMN_NAME_FLAGS = "flags";
        // Time stamp as returned by currentTimeMillis().
        public static final String COLUMN_NAME_TIMESTAMP = "timeStamp";
    }

    /* Data to be written to or read from a row in the table */
    public static class RowData {
        private static final int DEGREE_MODE = 2;
        private static final int LONG_TIMEOUT = 1;
        public final byte[] mExpression;
        public final int mFlags;
        public long mTimeStamp;  // 0 ==> this and next field to be filled in when written.
        private static int flagsFromDegreeAndTimeout(Boolean DegreeMode, Boolean LongTimeout) {
            return (DegreeMode ? DEGREE_MODE : 0) | (LongTimeout ? LONG_TIMEOUT : 0);
        }
        private boolean degreeModeFromFlags(int flags) {
            return (flags & DEGREE_MODE) != 0;
        }
        private boolean longTimeoutFromFlags(int flags) {
            return (flags & LONG_TIMEOUT) != 0;
        }
        private static final int MILLIS_IN_15_MINS = 15 * 60 * 1000;
        private RowData(byte[] expr, int flags, long timeStamp) {
            mExpression = expr;
            mFlags = flags;
            mTimeStamp = timeStamp;
        }
        /**
         * More client-friendly constructor that hides implementation ugliness.
         * utcOffset here is uncompressed, in milliseconds.
         * A zero timestamp will cause it to be automatically filled in.
         */
        public RowData(byte[] expr, boolean degreeMode, boolean longTimeout, long timeStamp) {
            this(expr, flagsFromDegreeAndTimeout(degreeMode, longTimeout), timeStamp);
        }
        public boolean degreeMode() {
            return degreeModeFromFlags(mFlags);
        }
        public boolean longTimeout() {
            return longTimeoutFromFlags(mFlags);
        }
        /**
         * Return a ContentValues object representing the current data.
         */
        public ContentValues toContentValues() {
            ContentValues cvs = new ContentValues();
            cvs.put(ExpressionEntry.COLUMN_NAME_EXPRESSION, mExpression);
            cvs.put(ExpressionEntry.COLUMN_NAME_FLAGS, mFlags);
            if (mTimeStamp == 0) {
                mTimeStamp = System.currentTimeMillis();
            }
            cvs.put(ExpressionEntry.COLUMN_NAME_TIMESTAMP, mTimeStamp);
            return cvs;
        }
    }

    private static final String SQL_CREATE_ENTRIES =
            "CREATE TABLE " + ExpressionEntry.TABLE_NAME + " ("
            + ExpressionEntry._ID + " INTEGER PRIMARY KEY,"
            + ExpressionEntry.COLUMN_NAME_EXPRESSION + " BLOB,"
            + ExpressionEntry.COLUMN_NAME_FLAGS + " INTEGER,"
            + ExpressionEntry.COLUMN_NAME_TIMESTAMP + " INTEGER)";
    private static final String SQL_DROP_TABLE =
            "DROP TABLE IF EXISTS " + ExpressionEntry.TABLE_NAME;
    private static final String SQL_GET_MIN = "SELECT MIN(" + ExpressionEntry._ID
            + ") FROM " + ExpressionEntry.TABLE_NAME;
    private static final String SQL_GET_MAX = "SELECT MAX(" + ExpressionEntry._ID
            + ") FROM " + ExpressionEntry.TABLE_NAME;
    private static final String SQL_GET_ROW = "SELECT * FROM " + ExpressionEntry.TABLE_NAME
            + " WHERE " + ExpressionEntry._ID + " = ?";
    private static final String SQL_GET_ALL = "SELECT * FROM " + ExpressionEntry.TABLE_NAME
            + " WHERE " + ExpressionEntry._ID + " <= ? AND " +
            ExpressionEntry._ID +  " >= ?" + " ORDER BY " + ExpressionEntry._ID + " DESC ";
    // We may eventually need an index by timestamp. We don't use it yet.
    private static final String SQL_CREATE_TIMESTAMP_INDEX =
            "CREATE INDEX timestamp_index ON " + ExpressionEntry.TABLE_NAME + "("
            + ExpressionEntry.COLUMN_NAME_TIMESTAMP + ")";
    private static final String SQL_DROP_TIMESTAMP_INDEX = "DROP INDEX IF EXISTS timestamp_index";

    private class ExpressionDBHelper extends SQLiteOpenHelper {
        // If you change the database schema, you must increment the database version.
        public static final int DATABASE_VERSION = 1;
        public static final String DATABASE_NAME = "Expressions.db";

        public ExpressionDBHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }
        public void onCreate(SQLiteDatabase db) {
            db.execSQL(SQL_CREATE_ENTRIES);
            db.execSQL(SQL_CREATE_TIMESTAMP_INDEX);
        }
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            // For now just throw away history on database version upgrade/downgrade.
            db.execSQL(SQL_DROP_TIMESTAMP_INDEX);
            db.execSQL(SQL_DROP_TABLE);
            onCreate(db);
        }
        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            onUpgrade(db, oldVersion, newVersion);
        }
    }

    private ExpressionDBHelper mExpressionDBHelper;

    private SQLiteDatabase mExpressionDB;  // Constant after initialization.

    // Expression indices between mMinAccessible and mMaxAccessible inclusive can be accessed.
    // We set these to more interesting values if a database access fails.
    // We punt on writes outside this range. We should never read outside this range.
    // If higher layers refer to an index outside this range, it will already be cached.
    // This also somewhat limits the size of the database, but only to an unreasonably
    // huge value.
    private long mMinAccessible = -10000000L;
    private long mMaxAccessible = 10000000L;

    // Never allocate new negative indicees (row ids) >= MAXIMUM_MIN_INDEX.
    public static final long MAXIMUM_MIN_INDEX = -10;

    // Minimum index value in DB.
    private long mMinIndex;
    // Maximum index value in DB.
    private long mMaxIndex;

    // A cursor that refers to the whole table, in reverse order.
    private AbstractWindowedCursor mAllCursor;

    // Expression index corresponding to a zero absolute offset for mAllCursor.
    // This is the argument we passed to the query.
    // We explicitly query only for entries that existed when we started, to avoid
    // interference from updates as we're running. It's unclear whether or not this matters.
    private int mAllCursorBase;

    // Database has been opened, mMinIndex and mMaxIndex are correct, mAllCursorBase and
    // mAllCursor have been set.
    private boolean mDBInitialized;

    // Gap between negative and positive row ids in the database.
    // Expressions with index [MAXIMUM_MIN_INDEX .. 0] are not stored.
    private static final long GAP = -MAXIMUM_MIN_INDEX + 1;

    // mLock protects mExpressionDB, mMinAccessible, and mMaxAccessible, mAllCursor,
    // mAllCursorBase, mMinIndex, mMaxIndex, and mDBInitialized. We access mExpressionDB without
    // synchronization after it's known to be initialized.  Used to wait for database
    // initialization.
    private Object mLock = new Object();

    public ExpressionDB(Context context) {
        mExpressionDBHelper = new ExpressionDBHelper(context);
        AsyncInitializer initializer = new AsyncInitializer();
        // All calls that create background database accesses are made from the UI thread, and
        // use a SERIAL_EXECUTOR. Thus they execute in order.
        initializer.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, mExpressionDBHelper);
    }

    // Is database completely unusable?
    private boolean isDBBad() {
        if (!CONTINUE_WITH_BAD_DB) {
            return false;
        }
        synchronized(mLock) {
            return mMinAccessible > mMaxAccessible;
        }
    }

    // Is the index in the accessible range of the database?
    private boolean inAccessibleRange(long index) {
        if (!CONTINUE_WITH_BAD_DB) {
            return true;
        }
        synchronized(mLock) {
            return index >= mMinAccessible && index <= mMaxAccessible;
        }
    }


    private void setBadDB() {
        if (!CONTINUE_WITH_BAD_DB) {
            Log.e("Calculator", "Database access failed");
            throw new RuntimeException("Database access failed");
        }
        displayDatabaseWarning();
        synchronized(mLock) {
            mMinAccessible = 1L;
            mMaxAccessible = -1L;
        }
    }

    /**
     * Initialize the database in the background.
     */
    private class AsyncInitializer extends AsyncTask<ExpressionDBHelper, Void, SQLiteDatabase> {
        @Override
        protected SQLiteDatabase doInBackground(ExpressionDBHelper... helper) {
            try {
                SQLiteDatabase db = helper[0].getWritableDatabase();
                synchronized(mLock) {
                    mExpressionDB = db;
                    try (Cursor minResult = db.rawQuery(SQL_GET_MIN, null)) {
                        if (!minResult.moveToFirst()) {
                            // Empty database.
                            mMinIndex = MAXIMUM_MIN_INDEX;
                        } else {
                            mMinIndex = Math.min(minResult.getLong(0), MAXIMUM_MIN_INDEX);
                        }
                    }
                    try (Cursor maxResult = db.rawQuery(SQL_GET_MAX, null)) {
                        if (!maxResult.moveToFirst()) {
                            // Empty database.
                            mMaxIndex = 0L;
                        } else {
                            mMaxIndex = Math.max(maxResult.getLong(0), 0L);
                        }
                    }
                    if (mMaxIndex > Integer.MAX_VALUE) {
                        throw new AssertionError("Expression index absurdly large");
                    }
                    mAllCursorBase = (int)mMaxIndex;
                    if (mMaxIndex != 0L || mMinIndex != MAXIMUM_MIN_INDEX) {
                        // Set up a cursor for reading the entire database.
                        String args[] = new String[]
                                { Long.toString(mAllCursorBase), Long.toString(mMinIndex) };
                        mAllCursor = (AbstractWindowedCursor) db.rawQuery(SQL_GET_ALL, args);
                        if (!mAllCursor.moveToFirst()) {
                            setBadDB();
                            return null;
                        }
                    }
                    mDBInitialized = true;
                    // We notify here, since there are unlikely cases in which the UI thread
                    // may be blocked on us, preventing onPostExecute from running.
                    mLock.notifyAll();
                }
                return db;
            } catch(SQLiteException e) {
                Log.e("Calculator", "Database initialization failed.\n", e);
                synchronized(mLock) {
                    setBadDB();
                    mLock.notifyAll();
                }
                return null;
            }
        }

        @Override
        protected void onPostExecute(SQLiteDatabase result) {
            if (result == null) {
                displayDatabaseWarning();
            } // else doInBackground already set expressionDB.
        }
        // On cancellation we do nothing;
    }

    private boolean databaseWarningIssued;

    /**
     * Display a warning message that a database access failed.
     * Do this only once. TODO: Replace with a real UI message.
     */
    void displayDatabaseWarning() {
        if (!databaseWarningIssued) {
            Log.e("Calculator", "Calculator restarting due to database error");
            databaseWarningIssued = true;
        }
    }

    /**
     * Wait until the database and mAllCursor, etc. have been initialized.
     */
    private void waitForDBInitialized() {
        synchronized(mLock) {
            // InterruptedExceptions are inconvenient here. Defer.
            boolean caught = false;
            while (!mDBInitialized && !isDBBad()) {
                try {
                    mLock.wait();
                } catch(InterruptedException e) {
                    caught = true;
                }
            }
            if (caught) {
                Thread.currentThread().interrupt();
            }
        }
    }

    /**
     * Erase the entire database. Assumes no other accesses to the database are
     * currently in progress
     * These tasks must be executed on a serial executor to avoid reordering writes.
     */
    private class AsyncEraser extends AsyncTask<Void, Void, Void> {
        @Override
        protected Void doInBackground(Void... nothings) {
            mExpressionDB.execSQL(SQL_DROP_TIMESTAMP_INDEX);
            mExpressionDB.execSQL(SQL_DROP_TABLE);
            try {
                mExpressionDB.execSQL("VACUUM");
            } catch(Exception e) {
                Log.v("Calculator", "Database VACUUM failed\n", e);
                // Should only happen with concurrent execution, which should be impossible.
            }
            mExpressionDB.execSQL(SQL_CREATE_ENTRIES);
            mExpressionDB.execSQL(SQL_CREATE_TIMESTAMP_INDEX);
            return null;
        }
        @Override
        protected void onPostExecute(Void nothing) {
            synchronized(mLock) {
                // Reinitialize everything to an empty and fully functional database.
                mMinAccessible = -10000000L;
                mMaxAccessible = 10000000L;
                mMinIndex = MAXIMUM_MIN_INDEX;
                mMaxIndex = mAllCursorBase = 0;
                mDBInitialized = true;
                mLock.notifyAll();
            }
        }
        // On cancellation we do nothing;
    }

    /**
     * Erase ALL database entries.
     * This is currently only safe if expressions that may refer to them are also erased.
     * Should only be called when concurrent references to the database are impossible.
     * TODO: Look at ways to more selectively clear the database.
     */
    public void eraseAll() {
        waitForDBInitialized();
        synchronized(mLock) {
            mDBInitialized = false;
        }
        AsyncEraser eraser = new AsyncEraser();
        eraser.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
    }

    // We track the number of outstanding writes to prevent onSaveInstanceState from
    // completing with in-flight database writes.

    private int mIncompleteWrites = 0;
    private Object mWriteCountsLock = new Object();  // Protects the preceding field.

    private void writeCompleted() {
        synchronized(mWriteCountsLock) {
            if (--mIncompleteWrites == 0) {
                mWriteCountsLock.notifyAll();
            }
        }
    }

    private void writeStarted() {
        synchronized(mWriteCountsLock) {
            ++mIncompleteWrites;
        }
    }

    /**
     * Wait for in-flight writes to complete.
     * This is not safe to call from one of our background tasks, since the writing
     * tasks may be waiting for the same underlying thread that we're using, resulting
     * in deadlock.
     */
    public void waitForWrites() {
        synchronized(mWriteCountsLock) {
            boolean caught = false;
            while (mIncompleteWrites != 0) {
                try {
                    mWriteCountsLock.wait();
                } catch (InterruptedException e) {
                    caught = true;
                }
            }
            if (caught) {
                Thread.currentThread().interrupt();
            }
        }
    }

    /**
     * Insert the given row in the database without blocking the UI thread.
     * These tasks must be executed on a serial executor to avoid reordering writes.
     */
    private class AsyncWriter extends AsyncTask<ContentValues, Void, Long> {
        @Override
        protected Long doInBackground(ContentValues... cvs) {
            long index = cvs[0].getAsLong(ExpressionEntry._ID);
            long result = mExpressionDB.insert(ExpressionEntry.TABLE_NAME, null, cvs[0]);
            writeCompleted();
            // Return 0 on success, row id on failure.
            if (result == -1) {
                return index;
            } else if (result != index) {
                throw new AssertionError("Expected row id " + index + ", got " + result);
            } else {
                return 0L;
            }
        }
        @Override
        protected void onPostExecute(Long result) {
            if (result != 0) {
                synchronized(mLock) {
                    if (result > 0) {
                        mMaxAccessible = result - 1;
                    } else {
                        mMinAccessible = result + 1;
                    }
                }
                displayDatabaseWarning();
            }
        }
        // On cancellation we do nothing;
    }

    /**
     * Add a row with index outside existing range.
     * The returned index will be just larger than any existing index unless negative_index is true.
     * In that case it will be smaller than any existing index and smaller than MAXIMUM_MIN_INDEX.
     * This ensures that prior additions have completed, but does not wait for this insertion
     * to complete.
     */
    public long addRow(boolean negativeIndex, RowData data) {
        long result;
        long newIndex;
        waitForDBInitialized();
        synchronized(mLock) {
            if (negativeIndex) {
                newIndex = mMinIndex - 1;
                mMinIndex = newIndex;
            } else {
                newIndex = mMaxIndex + 1;
                mMaxIndex = newIndex;
            }
            if (!inAccessibleRange(newIndex)) {
                // Just drop it, but go ahead and return a new index to use for the cache.
                // So long as reads of previously written expressions continue to work,
                // we should be fine. When the application is restarted, history will revert
                // to just include values between mMinAccessible and mMaxAccessible.
                return newIndex;
            }
            writeStarted();
            ContentValues cvs = data.toContentValues();
            cvs.put(ExpressionEntry._ID, newIndex);
            AsyncWriter awriter = new AsyncWriter();
            // Ensure that writes are executed in order.
            awriter.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, cvs);
        }
        return newIndex;
    }

    /**
     * Generate a fake database row that's good enough to hopefully prevent crashes,
     * but bad enough to avoid confusion with real data. In particular, the result
     * will fail to evaluate.
     */
    RowData makeBadRow() {
        CalculatorExpr badExpr = new CalculatorExpr();
        badExpr.add(R.id.lparen);
        badExpr.add(R.id.rparen);
        return new RowData(badExpr.toBytes(), false, false, 0);
    }

    /**
     * Retrieve the row with the given index using a direct query.
     * Such a row must exist.
     * We assume that the database has been initialized, and the argument has been range checked.
     */
    private RowData getRowDirect(long index) {
        RowData result;
        String args[] = new String[] { Long.toString(index) };
        try (Cursor resultC = mExpressionDB.rawQuery(SQL_GET_ROW, args)) {
            if (!resultC.moveToFirst()) {
                setBadDB();
                return makeBadRow();
            } else {
                result = new RowData(resultC.getBlob(1), resultC.getInt(2) /* flags */,
                        resultC.getLong(3) /* timestamp */);
            }
        }
        return result;
    }

    /**
     * Retrieve the row at the given offset from mAllCursorBase.
     * Note the argument is NOT an expression index!
     * We assume that the database has been initialized, and the argument has been range checked.
     */
    private RowData getRowFromCursor(int offset) {
        RowData result;
        synchronized(mLock) {
            if (!mAllCursor.moveToPosition(offset)) {
                Log.e("Calculator", "Failed to move cursor to position " + offset);
                setBadDB();
                return makeBadRow();
            }
            return new RowData(mAllCursor.getBlob(1), mAllCursor.getInt(2) /* flags */,
                        mAllCursor.getLong(3) /* timestamp */);
        }
    }

    /**
     * Retrieve the database row at the given index.
     * We currently assume that we never read data that we added since we initialized the database.
     * This makes sense, since we cache it anyway. And we should always cache recently added data.
     */
    public RowData getRow(long index) {
        waitForDBInitialized();
        if (!inAccessibleRange(index)) {
            // Even if something went wrong opening or writing the database, we should
            // not see such read requests, unless they correspond to a persistently
            // saved index, and we can't retrieve that expression.
            displayDatabaseWarning();
            return makeBadRow();
        }
        int position =  mAllCursorBase - (int)index;
        // We currently assume that the only gap between expression indices is the one around 0.
        if (index < 0) {
            position -= GAP;
        }
        if (position < 0) {
            throw new AssertionError("Database access out of range, index = " + index
                    + " rel. pos. = " + position);
        }
        if (index < 0) {
            // Avoid using mAllCursor to read data that's far away from the current position,
            // since we're likely to have to return to the current position.
            // This is a heuristic; we don't worry about doing the "wrong" thing in the race case.
            int endPosition;
            synchronized(mLock) {
                CursorWindow window = mAllCursor.getWindow();
                endPosition = window.getStartPosition() + window.getNumRows();
            }
            if (position >= endPosition) {
                return getRowDirect(index);
            }
        }
        // In the positive index case, it's probably OK to cross a cursor boundary, since
        // we're much more likely to stay in the new window.
        return getRowFromCursor(position);
    }

    public long getMinIndex() {
        waitForDBInitialized();
        synchronized(mLock) {
            return mMinIndex;
        }
    }

    public long getMaxIndex() {
        waitForDBInitialized();
        synchronized(mLock) {
            return mMaxIndex;
        }
    }

    public void close() {
        mExpressionDBHelper.close();
    }

}