Home | History | Annotate | Download | only in calculator2
      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 
     39 package com.android.calculator2;
     40 
     41 import android.app.Activity;
     42 import android.content.ContentValues;
     43 import android.content.Context;
     44 import android.database.AbstractWindowedCursor;
     45 import android.database.Cursor;
     46 import android.database.CursorWindow;
     47 import android.database.sqlite.SQLiteDatabase;
     48 import android.database.sqlite.SQLiteException;
     49 import android.database.sqlite.SQLiteOpenHelper;
     50 import android.os.AsyncTask;
     51 import android.provider.BaseColumns;
     52 import android.util.Log;
     53 import android.view.View;
     54 
     55 public 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