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