1 /* 2 * Copyright (C) 2015 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 package com.android.messaging.datamodel; 18 19 import android.content.ContentValues; 20 import android.content.Context; 21 import android.database.Cursor; 22 import android.database.DatabaseUtils; 23 import android.database.sqlite.SQLiteDatabase; 24 import android.database.sqlite.SQLiteFullException; 25 import android.database.sqlite.SQLiteQueryBuilder; 26 import android.database.sqlite.SQLiteStatement; 27 import android.util.SparseArray; 28 29 import com.android.messaging.Factory; 30 import com.android.messaging.R; 31 import com.android.messaging.util.Assert; 32 import com.android.messaging.util.BugleGservicesKeys; 33 import com.android.messaging.util.DebugUtils; 34 import com.android.messaging.util.LogUtil; 35 import com.android.messaging.util.UiUtils; 36 37 import java.util.Locale; 38 import java.util.Stack; 39 import java.util.regex.Pattern; 40 41 public class DatabaseWrapper { 42 private static final String TAG = LogUtil.BUGLE_DATABASE_TAG; 43 44 private final SQLiteDatabase mDatabase; 45 private final Context mContext; 46 private final boolean mLog; 47 /** 48 * Set mExplainQueryPlanRegexp (via {@link BugleGservicesKeys#EXPLAIN_QUERY_PLAN_REGEXP} 49 * to regex matching queries to see query plans. For example, ".*" to show all query plans. 50 */ 51 // See 52 private final String mExplainQueryPlanRegexp; 53 private static final int sTimingThreshold = 50; // in milliseconds 54 55 public static final int INDEX_INSERT_MESSAGE_PART = 0; 56 public static final int INDEX_INSERT_MESSAGE = 1; 57 public static final int INDEX_QUERY_CONVERSATIONS_LATEST_MESSAGE = 2; 58 public static final int INDEX_QUERY_MESSAGES_LATEST_MESSAGE = 3; 59 60 private final SparseArray<SQLiteStatement> mCompiledStatements; 61 62 static class TransactionData { 63 long time; 64 boolean transactionSuccessful; 65 } 66 67 // track transaction on a per thread basis 68 private static ThreadLocal<Stack<TransactionData>> sTransactionDepth = 69 new ThreadLocal<Stack<TransactionData>>() { 70 @Override 71 public Stack<TransactionData> initialValue() { 72 return new Stack<TransactionData>(); 73 } 74 }; 75 76 private static String[] sFormatStrings = new String[] { 77 "took %d ms to %s", 78 " took %d ms to %s", 79 " took %d ms to %s", 80 }; 81 82 DatabaseWrapper(final Context context, final SQLiteDatabase db) { 83 mLog = LogUtil.isLoggable(LogUtil.BUGLE_DATABASE_PERF_TAG, LogUtil.VERBOSE); 84 mExplainQueryPlanRegexp = Factory.get().getBugleGservices().getString( 85 BugleGservicesKeys.EXPLAIN_QUERY_PLAN_REGEXP, null); 86 mDatabase = db; 87 mContext = context; 88 mCompiledStatements = new SparseArray<SQLiteStatement>(); 89 } 90 91 public SQLiteStatement getStatementInTransaction(final int index, final String statement) { 92 // Use transaction to serialize access to statements 93 Assert.isTrue(mDatabase.inTransaction()); 94 SQLiteStatement compiled = mCompiledStatements.get(index); 95 if (compiled == null) { 96 compiled = mDatabase.compileStatement(statement); 97 Assert.isTrue(compiled.toString().contains(statement.trim())); 98 mCompiledStatements.put(index, compiled); 99 } 100 return compiled; 101 } 102 103 private void maybePlayDebugNoise() { 104 DebugUtils.maybePlayDebugNoise(mContext, DebugUtils.DEBUG_SOUND_DB_OP); 105 } 106 107 private static void printTiming(final long t1, final String msg) { 108 final int transactionDepth = sTransactionDepth.get().size(); 109 final long t2 = System.currentTimeMillis(); 110 final long delta = t2 - t1; 111 if (delta > sTimingThreshold) { 112 LogUtil.v(LogUtil.BUGLE_DATABASE_PERF_TAG, String.format(Locale.US, 113 sFormatStrings[Math.min(sFormatStrings.length - 1, transactionDepth)], 114 delta, 115 msg)); 116 } 117 } 118 119 public Context getContext() { 120 return mContext; 121 } 122 123 public void beginTransaction() { 124 final long t1 = System.currentTimeMillis(); 125 126 // push the current time onto the transaction stack 127 final TransactionData f = new TransactionData(); 128 f.time = t1; 129 sTransactionDepth.get().push(f); 130 131 mDatabase.beginTransaction(); 132 } 133 134 public void setTransactionSuccessful() { 135 final TransactionData f = sTransactionDepth.get().peek(); 136 f.transactionSuccessful = true; 137 mDatabase.setTransactionSuccessful(); 138 } 139 140 public void endTransaction() { 141 long t1 = 0; 142 long transactionStartTime = 0; 143 final TransactionData f = sTransactionDepth.get().pop(); 144 if (f.transactionSuccessful == false) { 145 LogUtil.w(TAG, "endTransaction without setting successful"); 146 for (final StackTraceElement st : (new Exception()).getStackTrace()) { 147 LogUtil.w(TAG, " " + st.toString()); 148 } 149 } 150 if (mLog) { 151 transactionStartTime = f.time; 152 t1 = System.currentTimeMillis(); 153 } 154 try { 155 mDatabase.endTransaction(); 156 } catch (SQLiteFullException ex) { 157 LogUtil.e(TAG, "Database full, unable to endTransaction", ex); 158 UiUtils.showToastAtBottom(R.string.db_full); 159 } 160 if (mLog) { 161 printTiming(t1, String.format(Locale.US, 162 ">>> endTransaction (total for this transaction: %d)", 163 (System.currentTimeMillis() - transactionStartTime))); 164 } 165 } 166 167 public void yieldTransaction() { 168 long yieldStartTime = 0; 169 if (mLog) { 170 yieldStartTime = System.currentTimeMillis(); 171 } 172 final boolean wasYielded = mDatabase.yieldIfContendedSafely(); 173 if (wasYielded && mLog) { 174 printTiming(yieldStartTime, "yieldTransaction"); 175 } 176 } 177 178 public void insertWithOnConflict(final String searchTable, final String nullColumnHack, 179 final ContentValues initialValues, final int conflictAlgorithm) { 180 long t1 = 0; 181 if (mLog) { 182 t1 = System.currentTimeMillis(); 183 } 184 try { 185 mDatabase.insertWithOnConflict(searchTable, nullColumnHack, initialValues, 186 conflictAlgorithm); 187 } catch (SQLiteFullException ex) { 188 LogUtil.e(TAG, "Database full, unable to insertWithOnConflict", ex); 189 UiUtils.showToastAtBottom(R.string.db_full); 190 } 191 if (mLog) { 192 printTiming(t1, String.format(Locale.US, 193 "insertWithOnConflict with ", searchTable)); 194 } 195 } 196 197 private void explainQueryPlan(final SQLiteQueryBuilder qb, final SQLiteDatabase db, 198 final String[] projection, final String selection, 199 @SuppressWarnings("unused") 200 final String[] queryArgs, 201 final String groupBy, 202 @SuppressWarnings("unused") 203 final String having, 204 final String sortOrder, final String limit) { 205 final String queryString = qb.buildQuery( 206 projection, 207 selection, 208 groupBy, 209 null/*having*/, 210 sortOrder, 211 limit); 212 explainQueryPlan(db, queryString, queryArgs); 213 } 214 215 private void explainQueryPlan(final SQLiteDatabase db, final String sql, 216 final String[] queryArgs) { 217 if (!Pattern.matches(mExplainQueryPlanRegexp, sql)) { 218 return; 219 } 220 final Cursor planCursor = db.rawQuery("explain query plan " + sql, queryArgs); 221 try { 222 if (planCursor != null && planCursor.moveToFirst()) { 223 final int detailColumn = planCursor.getColumnIndex("detail"); 224 final StringBuilder sb = new StringBuilder(); 225 do { 226 sb.append(planCursor.getString(detailColumn)); 227 sb.append("\n"); 228 } while (planCursor.moveToNext()); 229 if (sb.length() > 0) { 230 sb.setLength(sb.length() - 1); 231 } 232 LogUtil.v(TAG, "for query " + sql + "\nplan is: " 233 + sb.toString()); 234 } 235 } catch (final Exception e) { 236 LogUtil.w(TAG, "Query plan failed ", e); 237 } finally { 238 if (planCursor != null) { 239 planCursor.close(); 240 } 241 } 242 } 243 244 public Cursor query(final String searchTable, final String[] projection, 245 final String selection, final String[] selectionArgs, final String groupBy, 246 final String having, final String orderBy, final String limit) { 247 if (mExplainQueryPlanRegexp != null) { 248 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 249 qb.setTables(searchTable); 250 explainQueryPlan(qb, mDatabase, projection, selection, selectionArgs, 251 groupBy, having, orderBy, limit); 252 } 253 254 maybePlayDebugNoise(); 255 long t1 = 0; 256 if (mLog) { 257 t1 = System.currentTimeMillis(); 258 } 259 final Cursor cursor = mDatabase.query(searchTable, projection, selection, selectionArgs, 260 groupBy, having, orderBy, limit); 261 if (mLog) { 262 printTiming( 263 t1, 264 String.format(Locale.US, "query %s with %s ==> %d", 265 searchTable, selection, cursor.getCount())); 266 } 267 return cursor; 268 } 269 270 public Cursor query(final String searchTable, final String[] columns, 271 final String selection, final String[] selectionArgs, final String groupBy, 272 final String having, final String orderBy) { 273 return query( 274 searchTable, columns, selection, selectionArgs, 275 groupBy, having, orderBy, null); 276 } 277 278 public Cursor query(final SQLiteQueryBuilder qb, 279 final String[] projection, final String selection, final String[] queryArgs, 280 final String groupBy, final String having, final String sortOrder, final String limit) { 281 if (mExplainQueryPlanRegexp != null) { 282 explainQueryPlan(qb, mDatabase, projection, selection, queryArgs, 283 groupBy, having, sortOrder, limit); 284 } 285 maybePlayDebugNoise(); 286 long t1 = 0; 287 if (mLog) { 288 t1 = System.currentTimeMillis(); 289 } 290 final Cursor cursor = qb.query(mDatabase, projection, selection, queryArgs, groupBy, 291 having, sortOrder, limit); 292 if (mLog) { 293 printTiming( 294 t1, 295 String.format(Locale.US, "query %s with %s ==> %d", 296 qb.getTables(), selection, cursor.getCount())); 297 } 298 return cursor; 299 } 300 301 public long queryNumEntries(final String table, final String selection, 302 final String[] selectionArgs) { 303 long t1 = 0; 304 if (mLog) { 305 t1 = System.currentTimeMillis(); 306 } 307 maybePlayDebugNoise(); 308 final long retval = 309 DatabaseUtils.queryNumEntries(mDatabase, table, selection, selectionArgs); 310 if (mLog){ 311 printTiming( 312 t1, 313 String.format(Locale.US, "queryNumEntries %s with %s ==> %d", table, 314 selection, retval)); 315 } 316 return retval; 317 } 318 319 public Cursor rawQuery(final String sql, final String[] args) { 320 if (mExplainQueryPlanRegexp != null) { 321 explainQueryPlan(mDatabase, sql, args); 322 } 323 long t1 = 0; 324 if (mLog) { 325 t1 = System.currentTimeMillis(); 326 } 327 maybePlayDebugNoise(); 328 final Cursor cursor = mDatabase.rawQuery(sql, args); 329 if (mLog) { 330 printTiming( 331 t1, 332 String.format(Locale.US, "rawQuery %s ==> %d", sql, cursor.getCount())); 333 } 334 return cursor; 335 } 336 337 public int update(final String table, final ContentValues values, 338 final String selection, final String[] selectionArgs) { 339 long t1 = 0; 340 if (mLog) { 341 t1 = System.currentTimeMillis(); 342 } 343 maybePlayDebugNoise(); 344 int count = 0; 345 try { 346 count = mDatabase.update(table, values, selection, selectionArgs); 347 } catch (SQLiteFullException ex) { 348 LogUtil.e(TAG, "Database full, unable to update", ex); 349 UiUtils.showToastAtBottom(R.string.db_full); 350 } 351 if (mLog) { 352 printTiming(t1, String.format(Locale.US, "update %s with %s ==> %d", 353 table, selection, count)); 354 } 355 return count; 356 } 357 358 public int delete(final String table, final String whereClause, final String[] whereArgs) { 359 long t1 = 0; 360 if (mLog) { 361 t1 = System.currentTimeMillis(); 362 } 363 maybePlayDebugNoise(); 364 int count = 0; 365 try { 366 count = mDatabase.delete(table, whereClause, whereArgs); 367 } catch (SQLiteFullException ex) { 368 LogUtil.e(TAG, "Database full, unable to delete", ex); 369 UiUtils.showToastAtBottom(R.string.db_full); 370 } 371 if (mLog) { 372 printTiming(t1, 373 String.format(Locale.US, "delete from %s with %s ==> %d", table, 374 whereClause, count)); 375 } 376 return count; 377 } 378 379 public long insert(final String table, final String nullColumnHack, 380 final ContentValues values) { 381 long t1 = 0; 382 if (mLog) { 383 t1 = System.currentTimeMillis(); 384 } 385 maybePlayDebugNoise(); 386 long rowId = -1; 387 try { 388 rowId = mDatabase.insert(table, nullColumnHack, values); 389 } catch (SQLiteFullException ex) { 390 LogUtil.e(TAG, "Database full, unable to insert", ex); 391 UiUtils.showToastAtBottom(R.string.db_full); 392 } 393 if (mLog) { 394 printTiming(t1, String.format(Locale.US, "insert to %s", table)); 395 } 396 return rowId; 397 } 398 399 public long replace(final String table, final String nullColumnHack, 400 final ContentValues values) { 401 long t1 = 0; 402 if (mLog) { 403 t1 = System.currentTimeMillis(); 404 } 405 maybePlayDebugNoise(); 406 long rowId = -1; 407 try { 408 rowId = mDatabase.replace(table, nullColumnHack, values); 409 } catch (SQLiteFullException ex) { 410 LogUtil.e(TAG, "Database full, unable to replace", ex); 411 UiUtils.showToastAtBottom(R.string.db_full); 412 } 413 if (mLog) { 414 printTiming(t1, String.format(Locale.US, "replace to %s", table)); 415 } 416 return rowId; 417 } 418 419 public void setLocale(final Locale locale) { 420 mDatabase.setLocale(locale); 421 } 422 423 public void execSQL(final String sql, final String[] bindArgs) { 424 long t1 = 0; 425 if (mLog) { 426 t1 = System.currentTimeMillis(); 427 } 428 maybePlayDebugNoise(); 429 try { 430 mDatabase.execSQL(sql, bindArgs); 431 } catch (SQLiteFullException ex) { 432 LogUtil.e(TAG, "Database full, unable to execSQL", ex); 433 UiUtils.showToastAtBottom(R.string.db_full); 434 } 435 436 if (mLog) { 437 printTiming(t1, String.format(Locale.US, "execSQL %s", sql)); 438 } 439 } 440 441 public void execSQL(final String sql) { 442 long t1 = 0; 443 if (mLog) { 444 t1 = System.currentTimeMillis(); 445 } 446 maybePlayDebugNoise(); 447 try { 448 mDatabase.execSQL(sql); 449 } catch (SQLiteFullException ex) { 450 LogUtil.e(TAG, "Database full, unable to execSQL", ex); 451 UiUtils.showToastAtBottom(R.string.db_full); 452 } 453 454 if (mLog) { 455 printTiming(t1, String.format(Locale.US, "execSQL %s", sql)); 456 } 457 } 458 459 public int execSQLUpdateDelete(final String sql) { 460 long t1 = 0; 461 if (mLog) { 462 t1 = System.currentTimeMillis(); 463 } 464 maybePlayDebugNoise(); 465 final SQLiteStatement statement = mDatabase.compileStatement(sql); 466 int rowsUpdated = 0; 467 try { 468 rowsUpdated = statement.executeUpdateDelete(); 469 } catch (SQLiteFullException ex) { 470 LogUtil.e(TAG, "Database full, unable to execSQLUpdateDelete", ex); 471 UiUtils.showToastAtBottom(R.string.db_full); 472 } 473 if (mLog) { 474 printTiming(t1, String.format(Locale.US, "execSQLUpdateDelete %s", sql)); 475 } 476 return rowsUpdated; 477 } 478 479 public SQLiteDatabase getDatabase() { 480 return mDatabase; 481 } 482 } 483