blob: c08d5aabd206535695a81ecd1f5ab4247cec23fb [file] [log] [blame]
/*
* 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<SQLiteStatement> mCompiledStatements;
static class TransactionData {
long time;
boolean transactionSuccessful;
}
// track transaction on a per thread basis
private static ThreadLocal<Stack<TransactionData>> sTransactionDepth =
new ThreadLocal<Stack<TransactionData>>() {
@Override
public Stack<TransactionData> initialValue() {
return new Stack<TransactionData>();
}
};
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<SQLiteStatement>();
}
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 %s", 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;
}
}