/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.messaging.datamodel; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteFullException; import android.database.sqlite.SQLiteQueryBuilder; import android.database.sqlite.SQLiteStatement; import android.util.SparseArray; import com.android.messaging.Factory; import com.android.messaging.R; import com.android.messaging.util.Assert; import com.android.messaging.util.BugleGservicesKeys; import com.android.messaging.util.DebugUtils; import com.android.messaging.util.LogUtil; import com.android.messaging.util.UiUtils; import java.util.Locale; import java.util.Stack; import java.util.regex.Pattern; public class DatabaseWrapper { private static final String TAG = LogUtil.BUGLE_DATABASE_TAG; private final SQLiteDatabase mDatabase; private final Context mContext; private final boolean mLog; /** * Set mExplainQueryPlanRegexp (via {@link BugleGservicesKeys#EXPLAIN_QUERY_PLAN_REGEXP} * to regex matching queries to see query plans. For example, ".*" to show all query plans. */ // See private final String mExplainQueryPlanRegexp; private static final int sTimingThreshold = 50; // in milliseconds public static final int INDEX_INSERT_MESSAGE_PART = 0; public static final int INDEX_INSERT_MESSAGE = 1; public static final int INDEX_QUERY_CONVERSATIONS_LATEST_MESSAGE = 2; public static final int INDEX_QUERY_MESSAGES_LATEST_MESSAGE = 3; private final SparseArray mCompiledStatements; static class TransactionData { long time; boolean transactionSuccessful; } // track transaction on a per thread basis private static ThreadLocal> sTransactionDepth = new ThreadLocal>() { @Override public Stack initialValue() { return new Stack(); } }; private static String[] sFormatStrings = new String[] { "took %d ms to %s", " took %d ms to %s", " took %d ms to %s", }; DatabaseWrapper(final Context context, final SQLiteDatabase db) { mLog = LogUtil.isLoggable(LogUtil.BUGLE_DATABASE_PERF_TAG, LogUtil.VERBOSE); mExplainQueryPlanRegexp = Factory.get().getBugleGservices().getString( BugleGservicesKeys.EXPLAIN_QUERY_PLAN_REGEXP, null); mDatabase = db; mContext = context; mCompiledStatements = new SparseArray(); } public SQLiteStatement getStatementInTransaction(final int index, final String statement) { // Use transaction to serialize access to statements Assert.isTrue(mDatabase.inTransaction()); SQLiteStatement compiled = mCompiledStatements.get(index); if (compiled == null) { compiled = mDatabase.compileStatement(statement); Assert.isTrue(compiled.toString().contains(statement.trim())); mCompiledStatements.put(index, compiled); } return compiled; } private void maybePlayDebugNoise() { DebugUtils.maybePlayDebugNoise(mContext, DebugUtils.DEBUG_SOUND_DB_OP); } private static void printTiming(final long t1, final String msg) { final int transactionDepth = sTransactionDepth.get().size(); final long t2 = System.currentTimeMillis(); final long delta = t2 - t1; if (delta > sTimingThreshold) { LogUtil.v(LogUtil.BUGLE_DATABASE_PERF_TAG, String.format(Locale.US, sFormatStrings[Math.min(sFormatStrings.length - 1, transactionDepth)], delta, msg)); } } public Context getContext() { return mContext; } public void beginTransaction() { final long t1 = System.currentTimeMillis(); // push the current time onto the transaction stack final TransactionData f = new TransactionData(); f.time = t1; sTransactionDepth.get().push(f); mDatabase.beginTransaction(); } public void setTransactionSuccessful() { final TransactionData f = sTransactionDepth.get().peek(); f.transactionSuccessful = true; mDatabase.setTransactionSuccessful(); } public void endTransaction() { long t1 = 0; long transactionStartTime = 0; final TransactionData f = sTransactionDepth.get().pop(); if (f.transactionSuccessful == false) { LogUtil.w(TAG, "endTransaction without setting successful"); for (final StackTraceElement st : (new Exception()).getStackTrace()) { LogUtil.w(TAG, " " + st.toString()); } } if (mLog) { transactionStartTime = f.time; t1 = System.currentTimeMillis(); } try { mDatabase.endTransaction(); } catch (SQLiteFullException ex) { LogUtil.e(TAG, "Database full, unable to endTransaction", ex); UiUtils.showToastAtBottom(R.string.db_full); } if (mLog) { printTiming(t1, String.format(Locale.US, ">>> endTransaction (total for this transaction: %d)", (System.currentTimeMillis() - transactionStartTime))); } } public void yieldTransaction() { long yieldStartTime = 0; if (mLog) { yieldStartTime = System.currentTimeMillis(); } final boolean wasYielded = mDatabase.yieldIfContendedSafely(); if (wasYielded && mLog) { printTiming(yieldStartTime, "yieldTransaction"); } } public void insertWithOnConflict(final String searchTable, final String nullColumnHack, final ContentValues initialValues, final int conflictAlgorithm) { long t1 = 0; if (mLog) { t1 = System.currentTimeMillis(); } try { mDatabase.insertWithOnConflict(searchTable, nullColumnHack, initialValues, conflictAlgorithm); } catch (SQLiteFullException ex) { LogUtil.e(TAG, "Database full, unable to insertWithOnConflict", ex); UiUtils.showToastAtBottom(R.string.db_full); } if (mLog) { printTiming(t1, String.format(Locale.US, "insertWithOnConflict with ", searchTable)); } } private void explainQueryPlan(final SQLiteQueryBuilder qb, final SQLiteDatabase db, final String[] projection, final String selection, @SuppressWarnings("unused") final String[] queryArgs, final String groupBy, @SuppressWarnings("unused") final String having, final String sortOrder, final String limit) { final String queryString = qb.buildQuery( projection, selection, groupBy, null/*having*/, sortOrder, limit); explainQueryPlan(db, queryString, queryArgs); } private void explainQueryPlan(final SQLiteDatabase db, final String sql, final String[] queryArgs) { if (!Pattern.matches(mExplainQueryPlanRegexp, sql)) { return; } final Cursor planCursor = db.rawQuery("explain query plan " + sql, queryArgs); try { if (planCursor != null && planCursor.moveToFirst()) { final int detailColumn = planCursor.getColumnIndex("detail"); final StringBuilder sb = new StringBuilder(); do { sb.append(planCursor.getString(detailColumn)); sb.append("\n"); } while (planCursor.moveToNext()); if (sb.length() > 0) { sb.setLength(sb.length() - 1); } LogUtil.v(TAG, "for query " + sql + "\nplan is: " + sb.toString()); } } catch (final Exception e) { LogUtil.w(TAG, "Query plan failed ", e); } finally { if (planCursor != null) { planCursor.close(); } } } public Cursor query(final String searchTable, final String[] projection, final String selection, final String[] selectionArgs, final String groupBy, final String having, final String orderBy, final String limit) { if (mExplainQueryPlanRegexp != null) { final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); qb.setTables(searchTable); explainQueryPlan(qb, mDatabase, projection, selection, selectionArgs, groupBy, having, orderBy, limit); } maybePlayDebugNoise(); long t1 = 0; if (mLog) { t1 = System.currentTimeMillis(); } final Cursor cursor = mDatabase.query(searchTable, projection, selection, selectionArgs, groupBy, having, orderBy, limit); if (mLog) { printTiming( t1, String.format(Locale.US, "query %s with %s ==> %d", searchTable, selection, cursor.getCount())); } return cursor; } public Cursor query(final String searchTable, final String[] columns, final String selection, final String[] selectionArgs, final String groupBy, final String having, final String orderBy) { return query( searchTable, columns, selection, selectionArgs, groupBy, having, orderBy, null); } public Cursor query(final SQLiteQueryBuilder qb, final String[] projection, final String selection, final String[] queryArgs, final String groupBy, final String having, final String sortOrder, final String limit) { if (mExplainQueryPlanRegexp != null) { explainQueryPlan(qb, mDatabase, projection, selection, queryArgs, groupBy, having, sortOrder, limit); } maybePlayDebugNoise(); long t1 = 0; if (mLog) { t1 = System.currentTimeMillis(); } final Cursor cursor = qb.query(mDatabase, projection, selection, queryArgs, groupBy, having, sortOrder, limit); if (mLog) { printTiming( t1, String.format(Locale.US, "query %s with %s ==> %d", qb.getTables(), selection, cursor.getCount())); } return cursor; } public long queryNumEntries(final String table, final String selection, final String[] selectionArgs) { long t1 = 0; if (mLog) { t1 = System.currentTimeMillis(); } maybePlayDebugNoise(); final long retval = DatabaseUtils.queryNumEntries(mDatabase, table, selection, selectionArgs); if (mLog){ printTiming( t1, String.format(Locale.US, "queryNumEntries %s with %s ==> %d", table, selection, retval)); } return retval; } public Cursor rawQuery(final String sql, final String[] args) { if (mExplainQueryPlanRegexp != null) { explainQueryPlan(mDatabase, sql, args); } long t1 = 0; if (mLog) { t1 = System.currentTimeMillis(); } maybePlayDebugNoise(); final Cursor cursor = mDatabase.rawQuery(sql, args); if (mLog) { printTiming( t1, String.format(Locale.US, "rawQuery %s ==> %d", sql, cursor.getCount())); } return cursor; } public int update(final String table, final ContentValues values, final String selection, final String[] selectionArgs) { long t1 = 0; if (mLog) { t1 = System.currentTimeMillis(); } maybePlayDebugNoise(); int count = 0; try { count = mDatabase.update(table, values, selection, selectionArgs); } catch (SQLiteFullException ex) { LogUtil.e(TAG, "Database full, unable to update", ex); UiUtils.showToastAtBottom(R.string.db_full); } if (mLog) { printTiming(t1, String.format(Locale.US, "update %s with %s ==> %d", table, selection, count)); } return count; } public int delete(final String table, final String whereClause, final String[] whereArgs) { long t1 = 0; if (mLog) { t1 = System.currentTimeMillis(); } maybePlayDebugNoise(); int count = 0; try { count = mDatabase.delete(table, whereClause, whereArgs); } catch (SQLiteFullException ex) { LogUtil.e(TAG, "Database full, unable to delete", ex); UiUtils.showToastAtBottom(R.string.db_full); } if (mLog) { printTiming(t1, String.format(Locale.US, "delete from %s with %s ==> %d", table, whereClause, count)); } return count; } public long insert(final String table, final String nullColumnHack, final ContentValues values) { long t1 = 0; if (mLog) { t1 = System.currentTimeMillis(); } maybePlayDebugNoise(); long rowId = -1; try { rowId = mDatabase.insert(table, nullColumnHack, values); } catch (SQLiteFullException ex) { LogUtil.e(TAG, "Database full, unable to insert", ex); UiUtils.showToastAtBottom(R.string.db_full); } if (mLog) { printTiming(t1, String.format(Locale.US, "insert to %s", table)); } return rowId; } public long replace(final String table, final String nullColumnHack, final ContentValues values) { long t1 = 0; if (mLog) { t1 = System.currentTimeMillis(); } maybePlayDebugNoise(); long rowId = -1; try { rowId = mDatabase.replace(table, nullColumnHack, values); } catch (SQLiteFullException ex) { LogUtil.e(TAG, "Database full, unable to replace", ex); UiUtils.showToastAtBottom(R.string.db_full); } if (mLog) { printTiming(t1, String.format(Locale.US, "replace to %s", table)); } return rowId; } public void setLocale(final Locale locale) { mDatabase.setLocale(locale); } public void execSQL(final String sql, final String[] bindArgs) { long t1 = 0; if (mLog) { t1 = System.currentTimeMillis(); } maybePlayDebugNoise(); try { mDatabase.execSQL(sql, bindArgs); } catch (SQLiteFullException ex) { LogUtil.e(TAG, "Database full, unable to execSQL", ex); UiUtils.showToastAtBottom(R.string.db_full); } if (mLog) { printTiming(t1, String.format(Locale.US, "execSQL %s", sql)); } } public void execSQL(final String sql) { long t1 = 0; if (mLog) { t1 = System.currentTimeMillis(); } maybePlayDebugNoise(); try { mDatabase.execSQL(sql); } catch (SQLiteFullException ex) { LogUtil.e(TAG, "Database full, unable to execSQL", ex); UiUtils.showToastAtBottom(R.string.db_full); } if (mLog) { printTiming(t1, String.format(Locale.US, "execSQL %s", sql)); } } public int execSQLUpdateDelete(final String sql) { long t1 = 0; if (mLog) { t1 = System.currentTimeMillis(); } maybePlayDebugNoise(); final SQLiteStatement statement = mDatabase.compileStatement(sql); int rowsUpdated = 0; try { rowsUpdated = statement.executeUpdateDelete(); } catch (SQLiteFullException ex) { LogUtil.e(TAG, "Database full, unable to execSQLUpdateDelete", ex); UiUtils.showToastAtBottom(R.string.db_full); } if (mLog) { printTiming(t1, String.format(Locale.US, "execSQLUpdateDelete %s", sql)); } return rowsUpdated; } public SQLiteDatabase getDatabase() { return mDatabase; } }