Back to project page android-database-sqlcipher.
The source code is released under:
Apache License
If you think the Android project android-database-sqlcipher listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.
/* * Copyright (C) 2006 The Android Open Source Project *//w ww. jav a 2 s . c om * 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 net.sqlcipher.database; import net.sqlcipher.Cursor; import net.sqlcipher.CrossProcessCursorWrapper; import net.sqlcipher.DatabaseUtils; import net.sqlcipher.SQLException; import net.sqlcipher.database.SQLiteDebug.DbStats; import net.sqlcipher.database.SQLiteDatabaseHook; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; import java.lang.ref.WeakReference; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.locks.ReentrantLock; import java.util.regex.Pattern; import java.util.zip.ZipInputStream; import android.content.ContentValues; import android.content.Context; import android.os.Debug; import android.os.SystemClock; import android.text.TextUtils; import android.util.Config; import android.util.Log; import android.util.Pair; /** * Exposes methods to manage a SQLite database. * <p>SQLiteDatabase has methods to create, delete, execute SQL commands, and * perform other common database management tasks. * <p>See the Notepad sample application in the SDK for an example of creating * and managing a database. * <p> Database names must be unique within an application, not across all * applications. * * <h3>Localized Collation - ORDER BY</h3> * <p>In addition to SQLite's default <code>BINARY</code> collator, Android supplies * two more, <code>LOCALIZED</code>, which changes with the system's current locale * if you wire it up correctly (XXX a link needed!), and <code>UNICODE</code>, which * is the Unicode Collation Algorithm and not tailored to the current locale. */ public class SQLiteDatabase extends SQLiteClosable { private static final String TAG = "Database"; private static final int EVENT_DB_OPERATION = 52000; private static final int EVENT_DB_CORRUPT = 75004; public int status(int operation, boolean reset){ return native_status(operation, reset); } public void changePassword(String password) throws SQLiteException { native_rekey(password); } public void changePassword(char[] password) throws SQLiteException { native_rekey(password); } private static void loadICUData(Context context, File workingDir) { try { File icuDir = new File(workingDir, "icu"); if(!icuDir.exists()) icuDir.mkdirs(); File icuDataFile = new File(icuDir, "icudt46l.dat"); if(!icuDataFile.exists()) { ZipInputStream in = new ZipInputStream(context.getAssets().open("icudt46l.zip")); in.getNextEntry(); OutputStream out = new FileOutputStream(icuDataFile); byte[] buf = new byte[1024]; int len; while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); } in.close(); out.flush(); out.close(); } } catch (Exception e) { Log.e(TAG, "Error copying icu data file", e); } } public static void loadLibs (Context context) { loadLibs(context, context.getFilesDir()); } public static void loadLibs (Context context, File workingDir) { System.loadLibrary("stlport_shared"); System.loadLibrary("sqlcipher_android"); System.loadLibrary("database_sqlcipher"); boolean systemICUFileExists = new File("/system/usr/icu/icudt46l.dat").exists(); String icuRootPath = systemICUFileExists ? "/system/usr" : workingDir.getAbsolutePath(); setICURoot(icuRootPath); if(!systemICUFileExists){ loadICUData(context, workingDir); } } /** * Algorithms used in ON CONFLICT clause * http://www.sqlite.org/lang_conflict.html */ /** * When a constraint violation occurs, an immediate ROLLBACK occurs, * thus ending the current transaction, and the command aborts with a * return code of SQLITE_CONSTRAINT. If no transaction is active * (other than the implied transaction that is created on every command) * then this algorithm works the same as ABORT. */ public static final int CONFLICT_ROLLBACK = 1; /** * When a constraint violation occurs,no ROLLBACK is executed * so changes from prior commands within the same transaction * are preserved. This is the default behavior. */ public static final int CONFLICT_ABORT = 2; /** * When a constraint violation occurs, the command aborts with a return * code SQLITE_CONSTRAINT. But any changes to the database that * the command made prior to encountering the constraint violation * are preserved and are not backed out. */ public static final int CONFLICT_FAIL = 3; /** * When a constraint violation occurs, the one row that contains * the constraint violation is not inserted or changed. * But the command continues executing normally. Other rows before and * after the row that contained the constraint violation continue to be * inserted or updated normally. No error is returned. */ public static final int CONFLICT_IGNORE = 4; /** * When a UNIQUE constraint violation occurs, the pre-existing rows that * are causing the constraint violation are removed prior to inserting * or updating the current row. Thus the insert or update always occurs. * The command continues executing normally. No error is returned. * If a NOT NULL constraint violation occurs, the NULL value is replaced * by the default value for that column. If the column has no default * value, then the ABORT algorithm is used. If a CHECK constraint * violation occurs then the IGNORE algorithm is used. When this conflict * resolution strategy deletes rows in order to satisfy a constraint, * it does not invoke delete triggers on those rows. * This behavior might change in a future release. */ public static final int CONFLICT_REPLACE = 5; /** * use the following when no conflict action is specified. */ public static final int CONFLICT_NONE = 0; private static final String[] CONFLICT_VALUES = new String[] {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "}; /** * Maximum Length Of A LIKE Or GLOB Pattern * The pattern matching algorithm used in the default LIKE and GLOB implementation * of SQLite can exhibit O(N^2) performance (where N is the number of characters in * the pattern) for certain pathological cases. To avoid denial-of-service attacks * the length of the LIKE or GLOB pattern is limited to SQLITE_MAX_LIKE_PATTERN_LENGTH bytes. * The default value of this limit is 50000. A modern workstation can evaluate * even a pathological LIKE or GLOB pattern of 50000 bytes relatively quickly. * The denial of service problem only comes into play when the pattern length gets * into millions of bytes. Nevertheless, since most useful LIKE or GLOB patterns * are at most a few dozen bytes in length, paranoid application developers may * want to reduce this parameter to something in the range of a few hundred * if they know that external users are able to generate arbitrary patterns. */ public static final int SQLITE_MAX_LIKE_PATTERN_LENGTH = 50000; /** * Flag for {@link #openDatabase} to open the database for reading and writing. * If the disk is full, this may fail even before you actually write anything. * * {@more} Note that the value of this flag is 0, so it is the default. */ public static final int OPEN_READWRITE = 0x00000000; // update native code if changing /** * Flag for {@link #openDatabase} to open the database for reading only. * This is the only reliable way to open a database if the disk may be full. */ public static final int OPEN_READONLY = 0x00000001; // update native code if changing private static final int OPEN_READ_MASK = 0x00000001; // update native code if changing /** * Flag for {@link #openDatabase} to open the database without support for localized collators. * * {@more} This causes the collator <code>LOCALIZED</code> not to be created. * You must be consistent when using this flag to use the setting the database was * created with. If this is set, {@link #setLocale} will do nothing. */ public static final int NO_LOCALIZED_COLLATORS = 0x00000010; // update native code if changing /** * Flag for {@link #openDatabase} to create the database file if it does not already exist. */ public static final int CREATE_IF_NECESSARY = 0x10000000; // update native code if changing /** * Indicates whether the most-recently started transaction has been marked as successful. */ private boolean mInnerTransactionIsSuccessful; /** * Valid during the life of a transaction, and indicates whether the entire transaction (the * outer one and all of the inner ones) so far has been successful. */ private boolean mTransactionIsSuccessful; /** * Valid during the life of a transaction. */ private SQLiteTransactionListener mTransactionListener; /** Synchronize on this when accessing the database */ private final ReentrantLock mLock = new ReentrantLock(true); private long mLockAcquiredWallTime = 0L; private long mLockAcquiredThreadTime = 0L; // limit the frequency of complaints about each database to one within 20 sec // unless run command adb shell setprop log.tag.Database VERBOSE private static final int LOCK_WARNING_WINDOW_IN_MS = 20000; /** If the lock is held this long then a warning will be printed when it is released. */ private static final int LOCK_ACQUIRED_WARNING_TIME_IN_MS = 300; private static final int LOCK_ACQUIRED_WARNING_THREAD_TIME_IN_MS = 100; private static final int LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT = 2000; private static final int SLEEP_AFTER_YIELD_QUANTUM = 1000; // The pattern we remove from database filenames before // potentially logging them. private static final Pattern EMAIL_IN_DB_PATTERN = Pattern.compile("[\\w\\.\\-]+@[\\w\\.\\-]+"); private long mLastLockMessageTime = 0L; // Things related to query logging/sampling for debugging // slow/frequent queries during development. Always log queries // which take (by default) 500ms+; shorter queries are sampled // accordingly. Commit statements, which are typically slow, are // logged together with the most recently executed SQL statement, // for disambiguation. The 500ms value is configurable via a // SystemProperty, but developers actively debugging database I/O // should probably use the regular log tunable, // LOG_SLOW_QUERIES_PROPERTY, defined below. private static int sQueryLogTimeInMillis = 0; // lazily initialized private static final int QUERY_LOG_SQL_LENGTH = 64; private static final String COMMIT_SQL = "COMMIT;"; private final Random mRandom = new Random(); private String mLastSqlStatement = null; // String prefix for slow database query EventLog records that show // lock acquistions of the database. /* package */ static final String GET_LOCK_LOG_PREFIX = "GETLOCK:"; /** Used by native code, do not rename */ /* package */ int mNativeHandle = 0; /** Used to make temp table names unique */ /* package */ int mTempTableSequence = 0; /** The path for the database file */ private String mPath; /** The anonymized path for the database file for logging purposes */ private String mPathForLogs = null; // lazily populated /** The flags passed to open/create */ private int mFlags; /** The optional factory to use when creating new Cursors */ private CursorFactory mFactory; private WeakHashMap<SQLiteClosable, Object> mPrograms; /** * for each instance of this class, a cache is maintained to store * the compiled query statement ids returned by sqlite database. * key = sql statement with "?" for bind args * value = {@link SQLiteCompiledSql} * If an application opens the database and keeps it open during its entire life, then * there will not be an overhead of compilation of sql statements by sqlite. * * why is this cache NOT static? because sqlite attaches compiledsql statements to the * struct created when {@link SQLiteDatabase#openDatabase(String, CursorFactory, int)} is * invoked. * * this cache has an upper limit of mMaxSqlCacheSize (settable by calling the method * (@link setMaxCacheSize(int)}). its default is 0 - i.e., no caching by default because * most of the apps don't use "?" syntax in their sql, caching is not useful for them. */ /* package */ Map<String, SQLiteCompiledSql> mCompiledQueries = new HashMap<String, SQLiteCompiledSql>(); /** * @hide */ public static final int MAX_SQL_CACHE_SIZE = 250; private int mMaxSqlCacheSize = MAX_SQL_CACHE_SIZE; // max cache size per Database instance private int mCacheFullWarnings; private static final int MAX_WARNINGS_ON_CACHESIZE_CONDITION = 1; /** maintain stats about number of cache hits and misses */ private int mNumCacheHits; private int mNumCacheMisses; /** the following 2 members maintain the time when a database is opened and closed */ private String mTimeOpened = null; private String mTimeClosed = null; /** Used to find out where this object was created in case it never got closed. */ private Throwable mStackTrace = null; // System property that enables logging of slow queries. Specify the threshold in ms. private static final String LOG_SLOW_QUERIES_PROPERTY = "db.log.slow_query_threshold"; private final int mSlowQueryThreshold; /** * @param closable */ void addSQLiteClosable(SQLiteClosable closable) { lock(); try { mPrograms.put(closable, null); } finally { unlock(); } } void removeSQLiteClosable(SQLiteClosable closable) { lock(); try { mPrograms.remove(closable); } finally { unlock(); } } @Override protected void onAllReferencesReleased() { if (isOpen()) { if (SQLiteDebug.DEBUG_SQL_CACHE) { mTimeClosed = getTime(); } dbclose(); } } /** * Attempts to release memory that SQLite holds but does not require to * operate properly. Typically this memory will come from the page cache. * * @return the number of bytes actually released */ static public native int releaseMemory(); /** * Control whether or not the SQLiteDatabase is made thread-safe by using locks * around critical sections. This is pretty expensive, so if you know that your * DB will only be used by a single thread then you should set this to false. * The default is true. * @param lockingEnabled set to true to enable locks, false otherwise */ public void setLockingEnabled(boolean lockingEnabled) { mLockingEnabled = lockingEnabled; } /** * If set then the SQLiteDatabase is made thread-safe by using locks * around critical sections */ private boolean mLockingEnabled = true; /* package */ void onCorruption() { Log.e(TAG, "Removing corrupt database: " + mPath); // EventLog.writeEvent(EVENT_DB_CORRUPT, mPath); try { // Close the database (if we can), which will cause subsequent operations to fail. close(); } finally { // Delete the corrupt file. Don't re-create it now -- that would just confuse people // -- but the next time someone tries to open it, they can set it up from scratch. if (!mPath.equalsIgnoreCase(":memory")) { // delete is only for non-memory database files new File(mPath).delete(); } } } /** * Locks the database for exclusive access. The database lock must be held when * touch the native sqlite3* object since it is single threaded and uses * a polling lock contention algorithm. The lock is recursive, and may be acquired * multiple times by the same thread. This is a no-op if mLockingEnabled is false. * * @see #unlock() */ /* package */ void lock() { if (!mLockingEnabled) return; mLock.lock(); if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) { if (mLock.getHoldCount() == 1) { // Use elapsed real-time since the CPU may sleep when waiting for IO mLockAcquiredWallTime = SystemClock.elapsedRealtime(); mLockAcquiredThreadTime = Debug.threadCpuTimeNanos(); } } } /** * Locks the database for exclusive access. The database lock must be held when * touch the native sqlite3* object since it is single threaded and uses * a polling lock contention algorithm. The lock is recursive, and may be acquired * multiple times by the same thread. * * @see #unlockForced() */ private void lockForced() { mLock.lock(); if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) { if (mLock.getHoldCount() == 1) { // Use elapsed real-time since the CPU may sleep when waiting for IO mLockAcquiredWallTime = SystemClock.elapsedRealtime(); mLockAcquiredThreadTime = Debug.threadCpuTimeNanos(); } } } /** * Releases the database lock. This is a no-op if mLockingEnabled is false. * * @see #unlock() */ /* package */ void unlock() { if (!mLockingEnabled) return; if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) { if (mLock.getHoldCount() == 1) { checkLockHoldTime(); } } mLock.unlock(); } /** * Releases the database lock. * * @see #unlockForced() */ private void unlockForced() { if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) { if (mLock.getHoldCount() == 1) { checkLockHoldTime(); } } mLock.unlock(); } private void checkLockHoldTime() { // Use elapsed real-time since the CPU may sleep when waiting for IO long elapsedTime = SystemClock.elapsedRealtime(); long lockedTime = elapsedTime - mLockAcquiredWallTime; if (lockedTime < LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT && !Log.isLoggable(TAG, Log.VERBOSE) && (elapsedTime - mLastLockMessageTime) < LOCK_WARNING_WINDOW_IN_MS) { return; } if (lockedTime > LOCK_ACQUIRED_WARNING_TIME_IN_MS) { int threadTime = (int) ((Debug.threadCpuTimeNanos() - mLockAcquiredThreadTime) / 1000000); if (threadTime > LOCK_ACQUIRED_WARNING_THREAD_TIME_IN_MS || lockedTime > LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT) { mLastLockMessageTime = elapsedTime; String msg = "lock held on " + mPath + " for " + lockedTime + "ms. Thread time was " + threadTime + "ms"; if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING_STACK_TRACE) { Log.d(TAG, msg, new Exception()); } else { Log.d(TAG, msg); } } } } /** * Begins a transaction. Transactions can be nested. When the outer transaction is ended all of * the work done in that transaction and all of the nested transactions will be committed or * rolled back. The changes will be rolled back if any transaction is ended without being * marked as clean (by calling setTransactionSuccessful). Otherwise they will be committed. * * <p>Here is the standard idiom for transactions: * * <pre> * db.beginTransaction(); * try { * ... * db.setTransactionSuccessful(); * } finally { * db.endTransaction(); * } * </pre> */ public void beginTransaction() { beginTransactionWithListener(null /* transactionStatusCallback */); } /** * Begins a transaction. Transactions can be nested. When the outer transaction is ended all of * the work done in that transaction and all of the nested transactions will be committed or * rolled back. The changes will be rolled back if any transaction is ended without being * marked as clean (by calling setTransactionSuccessful). Otherwise they will be committed. * * <p>Here is the standard idiom for transactions: * * <pre> * db.beginTransactionWithListener(listener); * try { * ... * db.setTransactionSuccessful(); * } finally { * db.endTransaction(); * } * </pre> * @param transactionListener listener that should be notified when the transaction begins, * commits, or is rolled back, either explicitly or by a call to * {@link #yieldIfContendedSafely}. */ public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) { lockForced(); if (!isOpen()) { throw new IllegalStateException("database not open"); } boolean ok = false; try { // If this thread already had the lock then get out if (mLock.getHoldCount() > 1) { if (mInnerTransactionIsSuccessful) { String msg = "Cannot call beginTransaction between " + "calling setTransactionSuccessful and endTransaction"; IllegalStateException e = new IllegalStateException(msg); Log.e(TAG, "beginTransaction() failed", e); throw e; } ok = true; return; } // This thread didn't already have the lock, so begin a database // transaction now. execSQL("BEGIN EXCLUSIVE;"); mTransactionListener = transactionListener; mTransactionIsSuccessful = true; mInnerTransactionIsSuccessful = false; if (transactionListener != null) { try { transactionListener.onBegin(); } catch (RuntimeException e) { execSQL("ROLLBACK;"); throw e; } } ok = true; } finally { if (!ok) { // beginTransaction is called before the try block so we must release the lock in // the case of failure. unlockForced(); } } } /** * End a transaction. See beginTransaction for notes about how to use this and when transactions * are committed and rolled back. */ public void endTransaction() { if (!isOpen()) { throw new IllegalStateException("database not open"); } if (!mLock.isHeldByCurrentThread()) { throw new IllegalStateException("no transaction pending"); } try { if (mInnerTransactionIsSuccessful) { mInnerTransactionIsSuccessful = false; } else { mTransactionIsSuccessful = false; } if (mLock.getHoldCount() != 1) { return; } RuntimeException savedException = null; if (mTransactionListener != null) { try { if (mTransactionIsSuccessful) { mTransactionListener.onCommit(); } else { mTransactionListener.onRollback(); } } catch (RuntimeException e) { savedException = e; mTransactionIsSuccessful = false; } } if (mTransactionIsSuccessful) { execSQL(COMMIT_SQL); } else { try { execSQL("ROLLBACK;"); if (savedException != null) { throw savedException; } } catch (SQLException e) { if (Config.LOGD) { Log.d(TAG, "exception during rollback, maybe the DB previously " + "performed an auto-rollback"); } } } } finally { mTransactionListener = null; unlockForced(); if (Config.LOGV) { Log.v(TAG, "unlocked " + Thread.currentThread() + ", holdCount is " + mLock.getHoldCount()); } } } /** * Marks the current transaction as successful. Do not do any more database work between * calling this and calling endTransaction. Do as little non-database work as possible in that * situation too. If any errors are encountered between this and endTransaction the transaction * will still be committed. * * @throws IllegalStateException if the current thread is not in a transaction or the * transaction is already marked as successful. */ public void setTransactionSuccessful() { if (!isOpen()) { throw new IllegalStateException("database not open"); } if (!mLock.isHeldByCurrentThread()) { throw new IllegalStateException("no transaction pending"); } if (mInnerTransactionIsSuccessful) { throw new IllegalStateException( "setTransactionSuccessful may only be called once per call to beginTransaction"); } mInnerTransactionIsSuccessful = true; } /** * return true if there is a transaction pending */ public boolean inTransaction() { return mLock.getHoldCount() > 0; } /** * Checks if the database lock is held by this thread. * * @return true, if this thread is holding the database lock. */ public boolean isDbLockedByCurrentThread() { return mLock.isHeldByCurrentThread(); } /** * Checks if the database is locked by another thread. This is * just an estimate, since this status can change at any time, * including after the call is made but before the result has * been acted upon. * * @return true, if the database is locked by another thread */ public boolean isDbLockedByOtherThreads() { return !mLock.isHeldByCurrentThread() && mLock.isLocked(); } /** * Temporarily end the transaction to let other threads run. The transaction is assumed to be * successful so far. Do not call setTransactionSuccessful before calling this. When this * returns a new transaction will have been created but not marked as successful. * @return true if the transaction was yielded * @deprecated if the db is locked more than once (becuase of nested transactions) then the lock * will not be yielded. Use yieldIfContendedSafely instead. */ @Deprecated public boolean yieldIfContended() { return yieldIfContendedHelper(false /* do not check yielding */, -1 /* sleepAfterYieldDelay */); } /** * Temporarily end the transaction to let other threads run. The transaction is assumed to be * successful so far. Do not call setTransactionSuccessful before calling this. When this * returns a new transaction will have been created but not marked as successful. This assumes * that there are no nested transactions (beginTransaction has only been called once) and will * throw an exception if that is not the case. * @return true if the transaction was yielded */ public boolean yieldIfContendedSafely() { return yieldIfContendedHelper(true /* check yielding */, -1 /* sleepAfterYieldDelay*/); } /** * Temporarily end the transaction to let other threads run. The transaction is assumed to be * successful so far. Do not call setTransactionSuccessful before calling this. When this * returns a new transaction will have been created but not marked as successful. This assumes * that there are no nested transactions (beginTransaction has only been called once) and will * throw an exception if that is not the case. * @param sleepAfterYieldDelay if > 0, sleep this long before starting a new transaction if * the lock was actually yielded. This will allow other background threads to make some * more progress than they would if we started the transaction immediately. * @return true if the transaction was yielded */ public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) { return yieldIfContendedHelper(true /* check yielding */, sleepAfterYieldDelay); } private boolean yieldIfContendedHelper(boolean checkFullyYielded, long sleepAfterYieldDelay) { if (mLock.getQueueLength() == 0) { // Reset the lock acquire time since we know that the thread was willing to yield // the lock at this time. mLockAcquiredWallTime = SystemClock.elapsedRealtime(); mLockAcquiredThreadTime = Debug.threadCpuTimeNanos(); return false; } setTransactionSuccessful(); SQLiteTransactionListener transactionListener = mTransactionListener; endTransaction(); if (checkFullyYielded) { if (this.isDbLockedByCurrentThread()) { throw new IllegalStateException( "Db locked more than once. yielfIfContended cannot yield"); } } if (sleepAfterYieldDelay > 0) { // Sleep for up to sleepAfterYieldDelay milliseconds, waking up periodically to // check if anyone is using the database. If the database is not contended, // retake the lock and return. long remainingDelay = sleepAfterYieldDelay; while (remainingDelay > 0) { try { Thread.sleep(remainingDelay < SLEEP_AFTER_YIELD_QUANTUM ? remainingDelay : SLEEP_AFTER_YIELD_QUANTUM); } catch (InterruptedException e) { Thread.interrupted(); } remainingDelay -= SLEEP_AFTER_YIELD_QUANTUM; if (mLock.getQueueLength() == 0) { break; } } } beginTransactionWithListener(transactionListener); return true; } /** Maps table names to info about what to which _sync_time column to set * to NULL on an update. This is used to support syncing. */ private final Map<String, SyncUpdateInfo> mSyncUpdateInfo = new HashMap<String, SyncUpdateInfo>(); public Map<String, String> getSyncedTables() { synchronized(mSyncUpdateInfo) { HashMap<String, String> tables = new HashMap<String, String>(); for (String table : mSyncUpdateInfo.keySet()) { SyncUpdateInfo info = mSyncUpdateInfo.get(table); if (info.deletedTable != null) { tables.put(table, info.deletedTable); } } return tables; } } /** * Internal class used to keep track what needs to be marked as changed * when an update occurs. This is used for syncing, so the sync engine * knows what data has been updated locally. */ static private class SyncUpdateInfo { /** * Creates the SyncUpdateInfo class. * * @param masterTable The table to set _sync_time to NULL in * @param deletedTable The deleted table that corresponds to the * master table * @param foreignKey The key that refers to the primary key in table */ SyncUpdateInfo(String masterTable, String deletedTable, String foreignKey) { this.masterTable = masterTable; this.deletedTable = deletedTable; this.foreignKey = foreignKey; } /** The table containing the _sync_time column */ String masterTable; /** The deleted table that corresponds to the master table */ String deletedTable; /** The key in the local table the row in table. It may be _id, if table * is the local table. */ String foreignKey; } /** * Used to allow returning sub-classes of {@link Cursor} when calling query. */ public interface CursorFactory { /** * See * {@link SQLiteCursor#SQLiteCursor(SQLiteDatabase, SQLiteCursorDriver, * String, SQLiteQuery)}. */ public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery, String editTable, SQLiteQuery query); } /** * Open the database according to the flags {@link #OPEN_READWRITE} * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}. * * <p>Sets the locale of the database to the the system's current locale. * Call {@link #setLocale} if you would like something else.</p> * * @param path to database file to open and/or create * @param factory an optional factory class that is called to instantiate a * cursor when query is called, or null for default * @param flags to control database access mode * @return the newly opened database * @throws SQLiteException if the database cannot be opened */ public static SQLiteDatabase openDatabase(String path, String password, CursorFactory factory, int flags, SQLiteDatabaseHook databaseHook) { return openDatabase(path, password.toCharArray(), factory, flags, databaseHook); } public static SQLiteDatabase openDatabase(String path, char[] password, CursorFactory factory, int flags, SQLiteDatabaseHook databaseHook) { SQLiteDatabase sqliteDatabase = null; try { // Open the database. sqliteDatabase = new SQLiteDatabase(path, password, factory, flags, databaseHook); if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { sqliteDatabase.enableSqlTracing(path); } if (SQLiteDebug.DEBUG_SQL_TIME) { sqliteDatabase.enableSqlProfiling(path); } } catch (SQLiteDatabaseCorruptException e) { // Try to recover from this, if we can. // TODO: should we do this for other open failures? Log.e(TAG, "Deleting and re-creating corrupt database " + path, e); // EventLog.writeEvent(EVENT_DB_CORRUPT, path); if (!path.equalsIgnoreCase(":memory")) { // delete is only for non-memory database files new File(path).delete(); } sqliteDatabase = new SQLiteDatabase(path, password, factory, flags, databaseHook); } ActiveDatabases.getInstance().mActiveDatabases.add( new WeakReference<SQLiteDatabase>(sqliteDatabase)); return sqliteDatabase; } public static SQLiteDatabase openOrCreateDatabase(File file, String password, CursorFactory factory, SQLiteDatabaseHook databaseHook){ return openOrCreateDatabase(file.getPath(), password, factory, databaseHook); } public static SQLiteDatabase openOrCreateDatabase(String path, String password, CursorFactory factory, SQLiteDatabaseHook databaseHook) { return openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook); } public static SQLiteDatabase openOrCreateDatabase(String path, char[] password, CursorFactory factory, SQLiteDatabaseHook databaseHook) { return openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook); } /** * Equivalent to openDatabase(file.getPath(), factory, CREATE_IF_NECESSARY). */ public static SQLiteDatabase openOrCreateDatabase(File file, String password, CursorFactory factory) { return openOrCreateDatabase(file.getPath(), password, factory, null); } /** * Equivalent to openDatabase(path, factory, CREATE_IF_NECESSARY). */ public static SQLiteDatabase openOrCreateDatabase(String path, String password, CursorFactory factory) { return openDatabase(path, password.toCharArray(), factory, CREATE_IF_NECESSARY, null); } public static SQLiteDatabase openOrCreateDatabase(String path, char[] password, CursorFactory factory) { return openDatabase(path, password, factory, CREATE_IF_NECESSARY, null); } public static SQLiteDatabase openDatabase(String path, String password, CursorFactory factory, int flags) { return openDatabase(path, password.toCharArray(), factory, CREATE_IF_NECESSARY, null); } public static SQLiteDatabase openDatabase(String path, char[] password, CursorFactory factory, int flags) { return openDatabase(path, password, factory, CREATE_IF_NECESSARY, null); } /** * Create a memory backed SQLite database. Its contents will be destroyed * when the database is closed. * * <p>Sets the locale of the database to the the system's current locale. * Call {@link #setLocale} if you would like something else.</p> * * @param factory an optional factory class that is called to instantiate a * cursor when query is called * @return a SQLiteDatabase object, or null if the database can't be created */ public static SQLiteDatabase create(CursorFactory factory, String password) { // This is a magic string with special meaning for SQLite. return openDatabase(":memory:", password.toCharArray(), factory, CREATE_IF_NECESSARY); } public static SQLiteDatabase create(CursorFactory factory, char[] password) { return openDatabase(":memory:", password, factory, CREATE_IF_NECESSARY); } /** * Close the database. */ public void close() { if (!isOpen()) { return; // already closed } lock(); try { closeClosable(); // close this database instance - regardless of its reference count value onAllReferencesReleased(); } finally { unlock(); } } private void closeClosable() { /* deallocate all compiled sql statement objects from mCompiledQueries cache. * this should be done before de-referencing all {@link SQLiteClosable} objects * from this database object because calling * {@link SQLiteClosable#onAllReferencesReleasedFromContainer()} could cause the database * to be closed. sqlite doesn't let a database close if there are * any unfinalized statements - such as the compiled-sql objects in mCompiledQueries. */ deallocCachedSqlStatements(); Iterator<Map.Entry<SQLiteClosable, Object>> iter = mPrograms.entrySet().iterator(); while (iter.hasNext()) { Map.Entry<SQLiteClosable, Object> entry = iter.next(); SQLiteClosable program = entry.getKey(); if (program != null) { program.onAllReferencesReleasedFromContainer(); } } } /** * Native call to close the database. */ private native void dbclose(); /** * Gets the database version. * * @return the database version */ public int getVersion() { SQLiteStatement prog = null; lock(); if (!isOpen()) { throw new IllegalStateException("database not open"); } try { prog = new SQLiteStatement(this, "PRAGMA user_version;"); long version = prog.simpleQueryForLong(); return (int) version; } finally { if (prog != null) prog.close(); unlock(); } } /** * Sets the database version. * * @param version the new database version */ public void setVersion(int version) { execSQL("PRAGMA user_version = " + version); } /** * Returns the maximum size the database may grow to. * * @return the new maximum database size */ public long getMaximumSize() { SQLiteStatement prog = null; lock(); if (!isOpen()) { throw new IllegalStateException("database not open"); } try { prog = new SQLiteStatement(this, "PRAGMA max_page_count;"); long pageCount = prog.simpleQueryForLong(); return pageCount * getPageSize(); } finally { if (prog != null) prog.close(); unlock(); } } /** * Sets the maximum size the database will grow to. The maximum size cannot * be set below the current size. * * @param numBytes the maximum database size, in bytes * @return the new maximum database size */ public long setMaximumSize(long numBytes) { SQLiteStatement prog = null; lock(); if (!isOpen()) { throw new IllegalStateException("database not open"); } try { long pageSize = getPageSize(); long numPages = numBytes / pageSize; // If numBytes isn't a multiple of pageSize, bump up a page if ((numBytes % pageSize) != 0) { numPages++; } prog = new SQLiteStatement(this, "PRAGMA max_page_count = " + numPages); long newPageCount = prog.simpleQueryForLong(); return newPageCount * pageSize; } finally { if (prog != null) prog.close(); unlock(); } } /** * Returns the current database page size, in bytes. * * @return the database page size, in bytes */ public long getPageSize() { SQLiteStatement prog = null; lock(); if (!isOpen()) { throw new IllegalStateException("database not open"); } try { prog = new SQLiteStatement(this, "PRAGMA page_size;"); long size = prog.simpleQueryForLong(); return size; } finally { if (prog != null) prog.close(); unlock(); } } /** * Sets the database page size. The page size must be a power of two. This * method does not work if any data has been written to the database file, * and must be called right after the database has been created. * * @param numBytes the database page size, in bytes */ public void setPageSize(long numBytes) { execSQL("PRAGMA page_size = " + numBytes); } /** * Mark this table as syncable. When an update occurs in this table the * _sync_dirty field will be set to ensure proper syncing operation. * * @param table the table to mark as syncable * @param deletedTable The deleted table that corresponds to the * syncable table */ public void markTableSyncable(String table, String deletedTable) { markTableSyncable(table, "_id", table, deletedTable); } /** * Mark this table as syncable, with the _sync_dirty residing in another * table. When an update occurs in this table the _sync_dirty field of the * row in updateTable with the _id in foreignKey will be set to * ensure proper syncing operation. * * @param table an update on this table will trigger a sync time removal * @param foreignKey this is the column in table whose value is an _id in * updateTable * @param updateTable this is the table that will have its _sync_dirty */ public void markTableSyncable(String table, String foreignKey, String updateTable) { markTableSyncable(table, foreignKey, updateTable, null); } /** * Mark this table as syncable, with the _sync_dirty residing in another * table. When an update occurs in this table the _sync_dirty field of the * row in updateTable with the _id in foreignKey will be set to * ensure proper syncing operation. * * @param table an update on this table will trigger a sync time removal * @param foreignKey this is the column in table whose value is an _id in * updateTable * @param updateTable this is the table that will have its _sync_dirty * @param deletedTable The deleted table that corresponds to the * updateTable */ private void markTableSyncable(String table, String foreignKey, String updateTable, String deletedTable) { lock(); try { native_execSQL("SELECT _sync_dirty FROM " + updateTable + " LIMIT 0"); native_execSQL("SELECT " + foreignKey + " FROM " + table + " LIMIT 0"); } finally { unlock(); } SyncUpdateInfo info = new SyncUpdateInfo(updateTable, deletedTable, foreignKey); synchronized (mSyncUpdateInfo) { mSyncUpdateInfo.put(table, info); } } /** * Call for each row that is updated in a cursor. * * @param table the table the row is in * @param rowId the row ID of the updated row */ /* package */ void rowUpdated(String table, long rowId) { SyncUpdateInfo info; synchronized (mSyncUpdateInfo) { info = mSyncUpdateInfo.get(table); } if (info != null) { execSQL("UPDATE " + info.masterTable + " SET _sync_dirty=1 WHERE _id=(SELECT " + info.foreignKey + " FROM " + table + " WHERE _id=" + rowId + ")"); } } /** * Finds the name of the first table, which is editable. * * @param tables a list of tables * @return the first table listed */ public static String findEditTable(String tables) { if (!TextUtils.isEmpty(tables)) { // find the first word terminated by either a space or a comma int spacepos = tables.indexOf(' '); int commapos = tables.indexOf(','); if (spacepos > 0 && (spacepos < commapos || commapos < 0)) { return tables.substring(0, spacepos); } else if (commapos > 0 && (commapos < spacepos || spacepos < 0) ) { return tables.substring(0, commapos); } return tables; } else { throw new IllegalStateException("Invalid tables"); } } /** * Compiles an SQL statement into a reusable pre-compiled statement object. * The parameters are identical to {@link #execSQL(String)}. You may put ?s in the * statement and fill in those values with {@link SQLiteProgram#bindString} * and {@link SQLiteProgram#bindLong} each time you want to run the * statement. Statements may not return result sets larger than 1x1. * * @param sql The raw SQL statement, may contain ? for unknown values to be * bound later. * @return A pre-compiled {@link SQLiteStatement} object. Note that * {@link SQLiteStatement}s are not synchronized, see the documentation for more details. */ public SQLiteStatement compileStatement(String sql) throws SQLException { lock(); if (!isOpen()) { throw new IllegalStateException("database not open"); } try { return new SQLiteStatement(this, sql); } finally { unlock(); } } /** * Query the given URL, returning a {@link Cursor} over the result set. * * @param distinct true if you want each row to be unique, false otherwise. * @param table The table name to compile the query against. * @param columns A list of which columns to return. Passing null will * return all columns, which is discouraged to prevent reading * data from storage that isn't going to be used. * @param selection A filter declaring which rows to return, formatted as an * SQL WHERE clause (excluding the WHERE itself). Passing null * will return all rows for the given table. * @param selectionArgs You may include ?s in selection, which will be * replaced by the values from selectionArgs, in order that they * appear in the selection. The values will be bound as Strings. * @param groupBy A filter declaring how to group rows, formatted as an SQL * GROUP BY clause (excluding the GROUP BY itself). Passing null * will cause the rows to not be grouped. * @param having A filter declare which row groups to include in the cursor, * if row grouping is being used, formatted as an SQL HAVING * clause (excluding the HAVING itself). Passing null will cause * all row groups to be included, and is required when row * grouping is not being used. * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause * (excluding the ORDER BY itself). Passing null will use the * default sort order, which may be unordered. * @param limit Limits the number of rows returned by the query, * formatted as LIMIT clause. Passing null denotes no LIMIT clause. * @return A {@link Cursor} object, which is positioned before the first entry. Note that * {@link Cursor}s are not synchronized, see the documentation for more details. * @see Cursor */ public Cursor query(boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) { return queryWithFactory(null, distinct, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit); } /** * Query the given URL, returning a {@link Cursor} over the result set. * * @param cursorFactory the cursor factory to use, or null for the default factory * @param distinct true if you want each row to be unique, false otherwise. * @param table The table name to compile the query against. * @param columns A list of which columns to return. Passing null will * return all columns, which is discouraged to prevent reading * data from storage that isn't going to be used. * @param selection A filter declaring which rows to return, formatted as an * SQL WHERE clause (excluding the WHERE itself). Passing null * will return all rows for the given table. * @param selectionArgs You may include ?s in selection, which will be * replaced by the values from selectionArgs, in order that they * appear in the selection. The values will be bound as Strings. * @param groupBy A filter declaring how to group rows, formatted as an SQL * GROUP BY clause (excluding the GROUP BY itself). Passing null * will cause the rows to not be grouped. * @param having A filter declare which row groups to include in the cursor, * if row grouping is being used, formatted as an SQL HAVING * clause (excluding the HAVING itself). Passing null will cause * all row groups to be included, and is required when row * grouping is not being used. * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause * (excluding the ORDER BY itself). Passing null will use the * default sort order, which may be unordered. * @param limit Limits the number of rows returned by the query, * formatted as LIMIT clause. Passing null denotes no LIMIT clause. * @return A {@link Cursor} object, which is positioned before the first entry. Note that * {@link Cursor}s are not synchronized, see the documentation for more details. * @see Cursor */ public Cursor queryWithFactory(CursorFactory cursorFactory, boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) { if (!isOpen()) { throw new IllegalStateException("database not open"); } String sql = SQLiteQueryBuilder.buildQueryString( distinct, table, columns, selection, groupBy, having, orderBy, limit); return rawQueryWithFactory( cursorFactory, sql, selectionArgs, findEditTable(table)); } /** * Query the given table, returning a {@link Cursor} over the result set. * * @param table The table name to compile the query against. * @param columns A list of which columns to return. Passing null will * return all columns, which is discouraged to prevent reading * data from storage that isn't going to be used. * @param selection A filter declaring which rows to return, formatted as an * SQL WHERE clause (excluding the WHERE itself). Passing null * will return all rows for the given table. * @param selectionArgs You may include ?s in selection, which will be * replaced by the values from selectionArgs, in order that they * appear in the selection. The values will be bound as Strings. * @param groupBy A filter declaring how to group rows, formatted as an SQL * GROUP BY clause (excluding the GROUP BY itself). Passing null * will cause the rows to not be grouped. * @param having A filter declare which row groups to include in the cursor, * if row grouping is being used, formatted as an SQL HAVING * clause (excluding the HAVING itself). Passing null will cause * all row groups to be included, and is required when row * grouping is not being used. * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause * (excluding the ORDER BY itself). Passing null will use the * default sort order, which may be unordered. * @return A {@link Cursor} object, which is positioned before the first entry. Note that * {@link Cursor}s are not synchronized, see the documentation for more details. * @see Cursor */ public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy) { return query(false, table, columns, selection, selectionArgs, groupBy, having, orderBy, null /* limit */); } /** * Query the given table, returning a {@link Cursor} over the result set. * * @param table The table name to compile the query against. * @param columns A list of which columns to return. Passing null will * return all columns, which is discouraged to prevent reading * data from storage that isn't going to be used. * @param selection A filter declaring which rows to return, formatted as an * SQL WHERE clause (excluding the WHERE itself). Passing null * will return all rows for the given table. * @param selectionArgs You may include ?s in selection, which will be * replaced by the values from selectionArgs, in order that they * appear in the selection. The values will be bound as Strings. * @param groupBy A filter declaring how to group rows, formatted as an SQL * GROUP BY clause (excluding the GROUP BY itself). Passing null * will cause the rows to not be grouped. * @param having A filter declare which row groups to include in the cursor, * if row grouping is being used, formatted as an SQL HAVING * clause (excluding the HAVING itself). Passing null will cause * all row groups to be included, and is required when row * grouping is not being used. * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause * (excluding the ORDER BY itself). Passing null will use the * default sort order, which may be unordered. * @param limit Limits the number of rows returned by the query, * formatted as LIMIT clause. Passing null denotes no LIMIT clause. * @return A {@link Cursor} object, which is positioned before the first entry. Note that * {@link Cursor}s are not synchronized, see the documentation for more details. * @see Cursor */ public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) { return query(false, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit); } /** * Runs the provided SQL and returns a {@link Cursor} over the result set. * * @param sql the SQL query. The SQL string must not be ; terminated * @param selectionArgs You may include ?s in where clause in the query, * which will be replaced by the values from selectionArgs. The * values will be bound as Strings. * @return A {@link Cursor} object, which is positioned before the first entry. Note that * {@link Cursor}s are not synchronized, see the documentation for more details. */ public Cursor rawQuery(String sql, String[] selectionArgs) { return rawQueryWithFactory(null, sql, selectionArgs, null); } /** * Runs the provided SQL and returns a cursor over the result set. * * @param cursorFactory the cursor factory to use, or null for the default factory * @param sql the SQL query. The SQL string must not be ; terminated * @param selectionArgs You may include ?s in where clause in the query, * which will be replaced by the values from selectionArgs. The * values will be bound as Strings. * @param editTable the name of the first table, which is editable * @return A {@link Cursor} object, which is positioned before the first entry. Note that * {@link Cursor}s are not synchronized, see the documentation for more details. */ public Cursor rawQueryWithFactory( CursorFactory cursorFactory, String sql, String[] selectionArgs, String editTable) { if (!isOpen()) { throw new IllegalStateException("database not open"); } long timeStart = 0; if (Config.LOGV || mSlowQueryThreshold != -1) { timeStart = System.currentTimeMillis(); } SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable); Cursor cursor = null; try { cursor = driver.query( cursorFactory != null ? cursorFactory : mFactory, selectionArgs); } finally { if (Config.LOGV || mSlowQueryThreshold != -1) { // Force query execution int count = -1; if (cursor != null) { count = cursor.getCount(); } long duration = System.currentTimeMillis() - timeStart; if (Config.LOGV || duration >= mSlowQueryThreshold) { Log.v(TAG, "query (" + duration + " ms): " + driver.toString() + ", args are <redacted>, count is " + count); } } } return new CrossProcessCursorWrapper(cursor); } /** * Runs the provided SQL and returns a cursor over the result set. * The cursor will read an initial set of rows and the return to the caller. * It will continue to read in batches and send data changed notifications * when the later batches are ready. * @param sql the SQL query. The SQL string must not be ; terminated * @param selectionArgs You may include ?s in where clause in the query, * which will be replaced by the values from selectionArgs. The * values will be bound as Strings. * @param initialRead set the initial count of items to read from the cursor * @param maxRead set the count of items to read on each iteration after the first * @return A {@link Cursor} object, which is positioned before the first entry. Note that * {@link Cursor}s are not synchronized, see the documentation for more details. * * This work is incomplete and not fully tested or reviewed, so currently * hidden. * @hide */ public Cursor rawQuery(String sql, String[] selectionArgs, int initialRead, int maxRead) { net.sqlcipher.CursorWrapper cursorWrapper = (net.sqlcipher.CursorWrapper)rawQueryWithFactory(null, sql, selectionArgs, null); ((SQLiteCursor)cursorWrapper.getWrappedCursor()).setLoadStyle(initialRead, maxRead); return cursorWrapper; } /** * Convenience method for inserting a row into the database. * * @param table the table to insert the row into * @param nullColumnHack SQL doesn't allow inserting a completely empty row, * so if initialValues is empty this column will explicitly be * assigned a NULL value * @param values this map contains the initial column values for the * row. The keys should be the column names and the values the * column values * @return the row ID of the newly inserted row, or -1 if an error occurred */ public long insert(String table, String nullColumnHack, ContentValues values) { try { return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE); } catch (SQLException e) { Log.e(TAG, "Error inserting <redacted values> into " + table, e); return -1; } } /** * Convenience method for inserting a row into the database. * * @param table the table to insert the row into * @param nullColumnHack SQL doesn't allow inserting a completely empty row, * so if initialValues is empty this column will explicitly be * assigned a NULL value * @param values this map contains the initial column values for the * row. The keys should be the column names and the values the * column values * @throws SQLException * @return the row ID of the newly inserted row, or -1 if an error occurred */ public long insertOrThrow(String table, String nullColumnHack, ContentValues values) throws SQLException { return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE); } /** * Convenience method for replacing a row in the database. * * @param table the table in which to replace the row * @param nullColumnHack SQL doesn't allow inserting a completely empty row, * so if initialValues is empty this row will explicitly be * assigned a NULL value * @param initialValues this map contains the initial column values for * the row. The key * @return the row ID of the newly inserted row, or -1 if an error occurred */ public long replace(String table, String nullColumnHack, ContentValues initialValues) { try { return insertWithOnConflict(table, nullColumnHack, initialValues, CONFLICT_REPLACE); } catch (SQLException e) { Log.e(TAG, "Error inserting <redacted values> into " + table, e); return -1; } } /** * Convenience method for replacing a row in the database. * * @param table the table in which to replace the row * @param nullColumnHack SQL doesn't allow inserting a completely empty row, * so if initialValues is empty this row will explicitly be * assigned a NULL value * @param initialValues this map contains the initial column values for * the row. The key * @throws SQLException * @return the row ID of the newly inserted row, or -1 if an error occurred */ public long replaceOrThrow(String table, String nullColumnHack, ContentValues initialValues) throws SQLException { return insertWithOnConflict(table, nullColumnHack, initialValues, CONFLICT_REPLACE); } /** * General method for inserting a row into the database. * * @param table the table to insert the row into * @param nullColumnHack SQL doesn't allow inserting a completely empty row, * so if initialValues is empty this column will explicitly be * assigned a NULL value * @param initialValues this map contains the initial column values for the * row. The keys should be the column names and the values the * column values * @param conflictAlgorithm for insert conflict resolver * @return the row ID of the newly inserted row * OR the primary key of the existing row if the input param 'conflictAlgorithm' = * {@link #CONFLICT_IGNORE} * OR -1 if any error */ public long insertWithOnConflict(String table, String nullColumnHack, ContentValues initialValues, int conflictAlgorithm) { if (!isOpen()) { throw new IllegalStateException("database not open"); } // Measurements show most sql lengths <= 152 StringBuilder sql = new StringBuilder(152); sql.append("INSERT"); sql.append(CONFLICT_VALUES[conflictAlgorithm]); sql.append(" INTO "); sql.append(table); // Measurements show most values lengths < 40 StringBuilder values = new StringBuilder(40); Set<Map.Entry<String, Object>> entrySet = null; if (initialValues != null && initialValues.size() > 0) { entrySet = initialValues.valueSet(); Iterator<Map.Entry<String, Object>> entriesIter = entrySet.iterator(); sql.append('('); boolean needSeparator = false; while (entriesIter.hasNext()) { if (needSeparator) { sql.append(", "); values.append(", "); } needSeparator = true; Map.Entry<String, Object> entry = entriesIter.next(); sql.append(entry.getKey()); values.append('?'); } sql.append(')'); } else { sql.append("(" + nullColumnHack + ") "); values.append("NULL"); } sql.append(" VALUES("); sql.append(values); sql.append(");"); lock(); SQLiteStatement statement = null; try { statement = compileStatement(sql.toString()); // Bind the values if (entrySet != null) { int size = entrySet.size(); Iterator<Map.Entry<String, Object>> entriesIter = entrySet.iterator(); for (int i = 0; i < size; i++) { Map.Entry<String, Object> entry = entriesIter.next(); DatabaseUtils.bindObjectToProgram(statement, i + 1, entry.getValue()); } } // Run the program and then cleanup statement.execute(); long insertedRowId = lastInsertRow(); if (insertedRowId == -1) { Log.e(TAG, "Error inserting <redacted values> using <redacted sql> into " + table); } else { if (Config.LOGD && Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Inserting row " + insertedRowId + " from <redacted values> using <redacted sql> into " + table); } } return insertedRowId; } catch (SQLiteDatabaseCorruptException e) { onCorruption(); throw e; } finally { if (statement != null) { statement.close(); } unlock(); } } /** * Convenience method for deleting rows in the database. * * @param table the table to delete from * @param whereClause the optional WHERE clause to apply when deleting. * Passing null will delete all rows. * @return the number of rows affected if a whereClause is passed in, 0 * otherwise. To remove all rows and get a count pass "1" as the * whereClause. */ public int delete(String table, String whereClause, String[] whereArgs) { lock(); if (!isOpen()) { throw new IllegalStateException("database not open"); } SQLiteStatement statement = null; try { statement = compileStatement("DELETE FROM " + table + (!TextUtils.isEmpty(whereClause) ? " WHERE " + whereClause : "")); if (whereArgs != null) { int numArgs = whereArgs.length; for (int i = 0; i < numArgs; i++) { DatabaseUtils.bindObjectToProgram(statement, i + 1, whereArgs[i]); } } statement.execute(); return lastChangeCount(); } catch (SQLiteDatabaseCorruptException e) { onCorruption(); throw e; } finally { if (statement != null) { statement.close(); } unlock(); } } /** * Convenience method for updating rows in the database. * * @param table the table to update in * @param values a map from column names to new column values. null is a * valid value that will be translated to NULL. * @param whereClause the optional WHERE clause to apply when updating. * Passing null will update all rows. * @return the number of rows affected */ public int update(String table, ContentValues values, String whereClause, String[] whereArgs) { return updateWithOnConflict(table, values, whereClause, whereArgs, CONFLICT_NONE); } /** * Convenience method for updating rows in the database. * * @param table the table to update in * @param values a map from column names to new column values. null is a * valid value that will be translated to NULL. * @param whereClause the optional WHERE clause to apply when updating. * Passing null will update all rows. * @param conflictAlgorithm for update conflict resolver * @return the number of rows affected */ public int updateWithOnConflict(String table, ContentValues values, String whereClause, String[] whereArgs, int conflictAlgorithm) { if (values == null || values.size() == 0) { throw new IllegalArgumentException("Empty values"); } StringBuilder sql = new StringBuilder(120); sql.append("UPDATE "); sql.append(CONFLICT_VALUES[conflictAlgorithm]); sql.append(table); sql.append(" SET "); Set<Map.Entry<String, Object>> entrySet = values.valueSet(); Iterator<Map.Entry<String, Object>> entriesIter = entrySet.iterator(); while (entriesIter.hasNext()) { Map.Entry<String, Object> entry = entriesIter.next(); sql.append(entry.getKey()); sql.append("=?"); if (entriesIter.hasNext()) { sql.append(", "); } } if (!TextUtils.isEmpty(whereClause)) { sql.append(" WHERE "); sql.append(whereClause); } lock(); if (!isOpen()) { throw new IllegalStateException("database not open"); } SQLiteStatement statement = null; try { statement = compileStatement(sql.toString()); // Bind the values int size = entrySet.size(); entriesIter = entrySet.iterator(); int bindArg = 1; for (int i = 0; i < size; i++) { Map.Entry<String, Object> entry = entriesIter.next(); DatabaseUtils.bindObjectToProgram(statement, bindArg, entry.getValue()); bindArg++; } if (whereArgs != null) { size = whereArgs.length; for (int i = 0; i < size; i++) { statement.bindString(bindArg, whereArgs[i]); bindArg++; } } // Run the program and then cleanup statement.execute(); int numChangedRows = lastChangeCount(); if (Config.LOGD && Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Updated " + numChangedRows + " rows using <redacted values> and <redacted sql> for " + table); } return numChangedRows; } catch (SQLiteDatabaseCorruptException e) { onCorruption(); throw e; } catch (SQLException e) { Log.e(TAG, "Error updating <redacted values> using <redacted sql> for " + table); throw e; } finally { if (statement != null) { statement.close(); } unlock(); } } /** * Execute a single SQL statement that is not a query. For example, CREATE * TABLE, DELETE, INSERT, etc. Multiple statements separated by ;s are not * supported. it takes a write lock * * @throws SQLException If the SQL string is invalid for some reason */ public void execSQL(String sql) throws SQLException { long timeStart = SystemClock.uptimeMillis(); lock(); if (!isOpen()) { throw new IllegalStateException("database not open"); } logTimeStat(mLastSqlStatement, timeStart, GET_LOCK_LOG_PREFIX); try { native_execSQL(sql); } catch (SQLiteDatabaseCorruptException e) { onCorruption(); throw e; } finally { unlock(); } // Log commit statements along with the most recently executed // SQL statement for disambiguation. Note that instance // equality to COMMIT_SQL is safe here. if (sql == COMMIT_SQL) { logTimeStat(mLastSqlStatement, timeStart, COMMIT_SQL); } else { logTimeStat(sql, timeStart, null); } } public void rawExecSQL(String sql){ long timeStart = SystemClock.uptimeMillis(); lock(); if (!isOpen()) { throw new IllegalStateException("database not open"); } logTimeStat(mLastSqlStatement, timeStart, GET_LOCK_LOG_PREFIX); try { native_rawExecSQL(sql); } catch (SQLiteDatabaseCorruptException e) { onCorruption(); throw e; } finally { unlock(); } // Log commit statements along with the most recently executed // SQL statement for disambiguation. Note that instance // equality to COMMIT_SQL is safe here. if (sql == COMMIT_SQL) { logTimeStat(mLastSqlStatement, timeStart, COMMIT_SQL); } else { logTimeStat(sql, timeStart, null); } } /** * Execute a single SQL statement that is not a query. For example, CREATE * TABLE, DELETE, INSERT, etc. Multiple statements separated by ;s are not * supported. it takes a write lock, * * @param sql * @param bindArgs only byte[], String, Long and Double are supported in bindArgs. * @throws SQLException If the SQL string is invalid for some reason */ public void execSQL(String sql, Object[] bindArgs) throws SQLException { if (bindArgs == null) { throw new IllegalArgumentException("Empty bindArgs"); } long timeStart = SystemClock.uptimeMillis(); lock(); if (!isOpen()) { throw new IllegalStateException("database not open"); } SQLiteStatement statement = null; try { statement = compileStatement(sql); if (bindArgs != null) { int numArgs = bindArgs.length; for (int i = 0; i < numArgs; i++) { DatabaseUtils.bindObjectToProgram(statement, i + 1, bindArgs[i]); } } statement.execute(); } catch (SQLiteDatabaseCorruptException e) { onCorruption(); throw e; } finally { if (statement != null) { statement.close(); } unlock(); } logTimeStat(sql, timeStart); } @Override protected void finalize() { if (isOpen()) { Log.e(TAG, "close() was never explicitly called on database '" + mPath + "' ", mStackTrace); closeClosable(); onAllReferencesReleased(); } } public SQLiteDatabase(String path, char[] password, CursorFactory factory, int flags) { this(path, password, factory, flags, null); } /** * Private constructor. See {@link #create} and {@link #openDatabase}. * * @param path The full path to the database * @param factory The factory to use when creating cursors, may be NULL. * @param flags 0 or {@link #NO_LOCALIZED_COLLATORS}. If the database file already * exists, mFlags will be updated appropriately. */ public SQLiteDatabase(String path, char[] password, CursorFactory factory, int flags, SQLiteDatabaseHook databaseHook) { if (path == null) { throw new IllegalArgumentException("path should not be null"); } mFlags = flags; mPath = path; mSlowQueryThreshold = -1;//SystemProperties.getInt(LOG_SLOW_QUERIES_PROPERTY, -1); mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); mFactory = factory; mPrograms = new WeakHashMap<SQLiteClosable,Object>(); dbopen(mPath, mFlags); if(databaseHook != null){ databaseHook.preKey(this); } native_key(password); if(databaseHook != null){ databaseHook.postKey(this); } if (SQLiteDebug.DEBUG_SQL_CACHE) { mTimeOpened = getTime(); } try { setLocale(Locale.getDefault()); } catch (RuntimeException e) { Log.e(TAG, "Failed to setLocale() when constructing, closing the database", e); dbclose(); if (SQLiteDebug.DEBUG_SQL_CACHE) { mTimeClosed = getTime(); } throw e; } } private String getTime() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS ").format(System.currentTimeMillis()); } /** * return whether the DB is opened as read only. * @return true if DB is opened as read only */ public boolean isReadOnly() { return (mFlags & OPEN_READ_MASK) == OPEN_READONLY; } /** * @return true if the DB is currently open (has not been closed) */ public boolean isOpen() { return mNativeHandle != 0; } public boolean needUpgrade(int newVersion) { return newVersion > getVersion(); } /** * Getter for the path to the database file. * * @return the path to our database file. */ public final String getPath() { return mPath; } /* package */ void logTimeStat(String sql, long beginMillis) { logTimeStat(sql, beginMillis, null); } /* package */ void logTimeStat(String sql, long beginMillis, String prefix) { // Keep track of the last statement executed here, as this is // the common funnel through which all methods of hitting // libsqlite eventually flow. mLastSqlStatement = sql; // Sample fast queries in proportion to the time taken. // Quantize the % first, so the logged sampling probability // exactly equals the actual sampling rate for this query. int samplePercent; long durationMillis = SystemClock.uptimeMillis() - beginMillis; if (durationMillis == 0 && prefix == GET_LOCK_LOG_PREFIX) { // The common case is locks being uncontended. Don't log those, // even at 1%, which is our default below. return; } if (sQueryLogTimeInMillis == 0) { sQueryLogTimeInMillis = 500;//SystemProperties.getInt("db.db_operation.threshold_ms", 500); } if (durationMillis >= sQueryLogTimeInMillis) { samplePercent = 100; } else {; samplePercent = (int) (100 * durationMillis / sQueryLogTimeInMillis) + 1; if (mRandom.nextInt(100) >= samplePercent) return; } // Note: the prefix will be "COMMIT;" or "GETLOCK:" when non-null. We wait to do // it here so we avoid allocating in the common case. if (prefix != null) { sql = prefix + sql; } if (sql.length() > QUERY_LOG_SQL_LENGTH) sql = sql.substring(0, QUERY_LOG_SQL_LENGTH); // ActivityThread.currentPackageName() only returns non-null if the // current thread is an application main thread. This parameter tells // us whether an event loop is blocked, and if so, which app it is. // // Sadly, there's no fast way to determine app name if this is *not* a // main thread, or when we are invoked via Binder (e.g. ContentProvider). // Hopefully the full path to the database will be informative enough. //TODO get the current package name String blockingPackage = "unknown";//ActivityThread.currentPackageName(); if (blockingPackage == null) blockingPackage = ""; /* EventLog.writeEvent( EVENT_DB_OPERATION, getPathForLogs(), sql, durationMillis, blockingPackage, samplePercent);*/ } /** * Removes email addresses from database filenames before they're * logged to the EventLog where otherwise apps could potentially * read them. */ private String getPathForLogs() { if (mPathForLogs != null) { return mPathForLogs; } if (mPath == null) { return null; } if (mPath.indexOf('@') == -1) { mPathForLogs = mPath; } else { mPathForLogs = EMAIL_IN_DB_PATTERN.matcher(mPath).replaceAll("XX@YY"); } return mPathForLogs; } /** * Sets the locale for this database. Does nothing if this database has * the NO_LOCALIZED_COLLATORS flag set or was opened read only. * @throws SQLException if the locale could not be set. The most common reason * for this is that there is no collator available for the locale you requested. * In this case the database remains unchanged. */ public void setLocale(Locale locale) { lock(); try { native_setLocale(locale.toString(), mFlags); } finally { unlock(); } } /* * ============================================================================ * * The following methods deal with compiled-sql cache * ============================================================================ */ /** * adds the given sql and its compiled-statement-id-returned-by-sqlite to the * cache of compiledQueries attached to 'this'. * * if there is already a {@link SQLiteCompiledSql} in compiledQueries for the given sql, * the new {@link SQLiteCompiledSql} object is NOT inserted into the cache (i.e.,the current * mapping is NOT replaced with the new mapping). */ /* package */ void addToCompiledQueries(String sql, SQLiteCompiledSql compiledStatement) { if (mMaxSqlCacheSize == 0) { // for this database, there is no cache of compiled sql. if (SQLiteDebug.DEBUG_SQL_CACHE) { Log.v(TAG, "|NOT adding_sql_to_cache|" + getPath() + "|" + sql); } return; } SQLiteCompiledSql compiledSql = null; synchronized(mCompiledQueries) { // don't insert the new mapping if a mapping already exists compiledSql = mCompiledQueries.get(sql); if (compiledSql != null) { return; } // add this <sql, compiledStatement> to the cache if (mCompiledQueries.size() == mMaxSqlCacheSize) { /* * cache size of {@link #mMaxSqlCacheSize} is not enough for this app. * log a warning MAX_WARNINGS_ON_CACHESIZE_CONDITION times * chances are it is NOT using ? for bindargs - so caching is useless. * TODO: either let the callers set max cchesize for their app, or intelligently * figure out what should be cached for a given app. */ if (++mCacheFullWarnings == MAX_WARNINGS_ON_CACHESIZE_CONDITION) { Log.w(TAG, "Reached MAX size for compiled-sql statement cache for database " + getPath() + "; i.e., NO space for this sql statement in cache: " + sql + ". Please change your sql statements to use '?' for " + "bindargs, instead of using actual values"); } // don't add this entry to cache } else { // cache is NOT full. add this to cache. mCompiledQueries.put(sql, compiledStatement); if (SQLiteDebug.DEBUG_SQL_CACHE) { Log.v(TAG, "|adding_sql_to_cache|" + getPath() + "|" + mCompiledQueries.size() + "|" + sql); } } } return; } private void deallocCachedSqlStatements() { synchronized (mCompiledQueries) { for (SQLiteCompiledSql compiledSql : mCompiledQueries.values()) { compiledSql.releaseSqlStatement(); } mCompiledQueries.clear(); } } /** * from the compiledQueries cache, returns the compiled-statement-id for the given sql. * returns null, if not found in the cache. */ /* package */ SQLiteCompiledSql getCompiledStatementForSql(String sql) { SQLiteCompiledSql compiledStatement = null; boolean cacheHit; synchronized(mCompiledQueries) { if (mMaxSqlCacheSize == 0) { // for this database, there is no cache of compiled sql. if (SQLiteDebug.DEBUG_SQL_CACHE) { Log.v(TAG, "|cache NOT found|" + getPath()); } return null; } cacheHit = (compiledStatement = mCompiledQueries.get(sql)) != null; } if (cacheHit) { mNumCacheHits++; } else { mNumCacheMisses++; } if (SQLiteDebug.DEBUG_SQL_CACHE) { Log.v(TAG, "|cache_stats|" + getPath() + "|" + mCompiledQueries.size() + "|" + mNumCacheHits + "|" + mNumCacheMisses + "|" + cacheHit + "|" + mTimeOpened + "|" + mTimeClosed + "|" + sql); } return compiledStatement; } /** * returns true if the given sql is cached in compiled-sql cache. * @hide */ public boolean isInCompiledSqlCache(String sql) { synchronized(mCompiledQueries) { return mCompiledQueries.containsKey(sql); } } /** * purges the given sql from the compiled-sql cache. * @hide */ public void purgeFromCompiledSqlCache(String sql) { synchronized(mCompiledQueries) { mCompiledQueries.remove(sql); } } /** * remove everything from the compiled sql cache * @hide */ public void resetCompiledSqlCache() { synchronized(mCompiledQueries) { mCompiledQueries.clear(); } } /** * return the current maxCacheSqlCacheSize * @hide */ public synchronized int getMaxSqlCacheSize() { return mMaxSqlCacheSize; } /** * set the max size of the compiled sql cache for this database after purging the cache. * (size of the cache = number of compiled-sql-statements stored in the cache). * * max cache size can ONLY be increased from its current size (default = 0). * if this method is called with smaller size than the current value of mMaxSqlCacheSize, * then IllegalStateException is thrown * * synchronized because we don't want t threads to change cache size at the same time. * @param cacheSize the size of the cache. can be (0 to MAX_SQL_CACHE_SIZE) * @throws IllegalStateException if input cacheSize > MAX_SQL_CACHE_SIZE or < 0 or * < the value set with previous setMaxSqlCacheSize() call. * * @hide */ public synchronized void setMaxSqlCacheSize(int cacheSize) { if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) { throw new IllegalStateException("expected value between 0 and " + MAX_SQL_CACHE_SIZE); } else if (cacheSize < mMaxSqlCacheSize) { throw new IllegalStateException("cannot set cacheSize to a value less than the value " + "set with previous setMaxSqlCacheSize() call."); } mMaxSqlCacheSize = cacheSize; } static class ActiveDatabases { private static final ActiveDatabases activeDatabases = new ActiveDatabases(); private HashSet<WeakReference<SQLiteDatabase>> mActiveDatabases = new HashSet<WeakReference<SQLiteDatabase>>(); private ActiveDatabases() {} // disable instantiation of this class static ActiveDatabases getInstance() {return activeDatabases;} } /** * this method is used to collect data about ALL open databases in the current process. * bugreport is a user of this data. */ /* package */ static ArrayList<DbStats> getDbStats() { ArrayList<DbStats> dbStatsList = new ArrayList<DbStats>(); for (WeakReference<SQLiteDatabase> w : ActiveDatabases.getInstance().mActiveDatabases) { SQLiteDatabase db = w.get(); if (db == null || !db.isOpen()) { continue; } // get SQLITE_DBSTATUS_LOOKASIDE_USED for the db int lookasideUsed = db.native_getDbLookaside(); // get the lastnode of the dbname String path = db.getPath(); int indx = path.lastIndexOf("/"); String lastnode = path.substring((indx != -1) ? ++indx : 0); // get list of attached dbs and for each db, get its size and pagesize ArrayList<Pair<String, String>> attachedDbs = getAttachedDbs(db); if (attachedDbs == null) { continue; } for (int i = 0; i < attachedDbs.size(); i++) { Pair<String, String> p = attachedDbs.get(i); long pageCount = getPragmaVal(db, p.first + ".page_count;"); // first entry in the attached db list is always the main database // don't worry about prefixing the dbname with "main" String dbName; if (i == 0) { dbName = lastnode; } else { // lookaside is only relevant for the main db lookasideUsed = 0; dbName = " (attached) " + p.first; // if the attached db has a path, attach the lastnode from the path to above if (p.second.trim().length() > 0) { int idx = p.second.lastIndexOf("/"); dbName += " : " + p.second.substring((idx != -1) ? ++idx : 0); } } if (pageCount > 0) { dbStatsList.add(new DbStats(dbName, pageCount, db.getPageSize(), lookasideUsed)); } } } return dbStatsList; } /** * get the specified pragma value from sqlite for the specified database. * only handles pragma's that return int/long. * NO JAVA locks are held in this method. * TODO: use this to do all pragma's in this class */ private static long getPragmaVal(SQLiteDatabase db, String pragma) { if (!db.isOpen()) { return 0; } SQLiteStatement prog = null; try { prog = new SQLiteStatement(db, "PRAGMA " + pragma); long val = prog.simpleQueryForLong(); return val; } finally { if (prog != null) prog.close(); } } /** * returns list of full pathnames of all attached databases * including the main database * TODO: move this to {@link DatabaseUtils} */ private static ArrayList<Pair<String, String>> getAttachedDbs(SQLiteDatabase dbObj) { if (!dbObj.isOpen()) { return null; } ArrayList<Pair<String, String>> attachedDbs = new ArrayList<Pair<String, String>>(); Cursor c = dbObj.rawQuery("pragma database_list;", null); while (c.moveToNext()) { attachedDbs.add(new Pair<String, String>(c.getString(1), c.getString(2))); } c.close(); return attachedDbs; } /** * Sets the root directory to search for the ICU data file */ public static native void setICURoot(String path); /** * Native call to open the database. * * @param path The full path to the database */ private native void dbopen(String path, int flags); /** * Native call to setup tracing of all sql statements * * @param path the full path to the database */ private native void enableSqlTracing(String path); /** * Native call to setup profiling of all sql statements. * currently, sqlite's profiling = printing of execution-time * (wall-clock time) of each of the sql statements, as they * are executed. * * @param path the full path to the database */ private native void enableSqlProfiling(String path); /** * Native call to execute a raw SQL statement. {@link #lock} must be held * when calling this method. * * @param sql The raw SQL string * @throws SQLException */ /* package */ native void native_execSQL(String sql) throws SQLException; /** * Native call to set the locale. {@link #lock} must be held when calling * this method. * @throws SQLException */ /* package */ native void native_setLocale(String loc, int flags); /** * Returns the row ID of the last row inserted into the database. * * @return the row ID of the last row inserted into the database. */ /* package */ native long lastInsertRow(); /** * Returns the number of changes made in the last statement executed. * * @return the number of changes made in the last statement executed. */ /* package */ native int lastChangeCount(); /** * return the SQLITE_DBSTATUS_LOOKASIDE_USED documented here * http://www.sqlite.org/c3ref/c_dbstatus_lookaside_used.html * @return int value of SQLITE_DBSTATUS_LOOKASIDE_USED */ private native int native_getDbLookaside(); private native void native_rawExecSQL(String sql); private native int native_status(int operation, boolean reset); private native void native_key(char[] key) throws SQLException; private native void native_key(String key) throws SQLException; private native void native_rekey(String key) throws SQLException; private native void native_rekey(char[] key) throws SQLException; }