• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
flagsFromDegreeAndTimeout(Boolean DegreeMode, Boolean LongTimeout)74         private static int flagsFromDegreeAndTimeout(Boolean DegreeMode, Boolean LongTimeout) {
75             return (DegreeMode ? DEGREE_MODE : 0) | (LongTimeout ? LONG_TIMEOUT : 0);
76         }
degreeModeFromFlags(int flags)77         private boolean degreeModeFromFlags(int flags) {
78             return (flags & DEGREE_MODE) != 0;
79         }
longTimeoutFromFlags(int flags)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;
RowData(byte[] expr, int flags, long timeStamp)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          */
RowData(byte[] expr, boolean degreeMode, boolean longTimeout, long timeStamp)94         public RowData(byte[] expr, boolean degreeMode, boolean longTimeout, long timeStamp) {
95             this(expr, flagsFromDegreeAndTimeout(degreeMode, longTimeout), timeStamp);
96         }
degreeMode()97         public boolean degreeMode() {
98             return degreeModeFromFlags(mFlags);
99         }
longTimeout()100         public boolean longTimeout() {
101             return longTimeoutFromFlags(mFlags);
102         }
103         /**
104          * Return a ContentValues object representing the current data.
105          */
toContentValues()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 
ExpressionDBHelper(Context context)146         public ExpressionDBHelper(Context context) {
147             super(context, DATABASE_NAME, null, DATABASE_VERSION);
148         }
onCreate(SQLiteDatabase db)149         public void onCreate(SQLiteDatabase db) {
150             db.execSQL(SQL_CREATE_ENTRIES);
151             db.execSQL(SQL_CREATE_TIMESTAMP_INDEX);
152         }
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)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         }
onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion)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 
ExpressionDB(Context context)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?
isDBBad()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?
inAccessibleRange(long index)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 
setBadDB()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
doInBackground(ExpressionDBHelper... helper)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
onPostExecute(SQLiteDatabase result)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      */
displayDatabaseWarning()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      */
waitForDBInitialized()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
doInBackground(Void... nothings)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
onPostExecute(Void nothing)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      */
eraseAll()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 
writeCompleted()403     private void writeCompleted() {
404         synchronized(mWriteCountsLock) {
405             if (--mIncompleteWrites == 0) {
406                 mWriteCountsLock.notifyAll();
407             }
408         }
409     }
410 
writeStarted()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      */
waitForWrites()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
doInBackground(ContentValues... cvs)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
onPostExecute(Long result)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      */
addRow(boolean negativeIndex, RowData data)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      */
makeBadRow()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      */
getRowDirect(long index)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      */
getRowFromCursor(int offset)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      */
getRow(long index)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 
getMinIndex()601     public long getMinIndex() {
602         waitForDBInitialized();
603         synchronized(mLock) {
604             return mMinIndex;
605         }
606     }
607 
getMaxIndex()608     public long getMaxIndex() {
609         waitForDBInitialized();
610         synchronized(mLock) {
611             return mMaxIndex;
612         }
613     }
614 
close()615     public void close() {
616         mExpressionDBHelper.close();
617     }
618 
619 }
620