/*
 * Copyright (C) 2006 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 net.sqlcipher.database;

import net.sqlcipher.BuildConfig;
import net.sqlcipher.Cursor;
import net.sqlcipher.CrossProcessCursorWrapper;
import net.sqlcipher.DatabaseUtils;
import net.sqlcipher.DatabaseErrorHandler;
import net.sqlcipher.DefaultDatabaseErrorHandler;
import net.sqlcipher.SQLException;
import net.sqlcipher.database.SQLiteDebug.DbStats;
import net.sqlcipher.database.SQLiteDatabaseHook;
import net.sqlcipher.database.SQLiteQueryStats;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
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;

import java.io.UnsupportedEncodingException;

/**
 * Exposes methods to manage a SQLCipher database.
 * <p>SQLiteDatabase has methods to create, delete, execute SQL commands, and
 * perform other common database management tasks.
 * <p>A call to <code>loadLibs(…)</code> should occur before attempting to
 * create or open a database connection.
 * <p> Database names must be unique within an application, not across all
 * applications.
 *
 */
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;
  private static final String KEY_ENCODING = "UTF-8";

  private enum SQLiteDatabaseTransactionType {
    Deferred,
    Immediate,
    Exclusive,
  }

  /**
   * The version number of the SQLCipher for Android Java client library.
   */
  public static final String SQLCIPHER_ANDROID_VERSION = BuildConfig.VERSION_NAME;

  // Stores reference to all databases opened in the current process.
  // (The referent Object is not used at this time.)
  // INVARIANT: Guarded by sActiveDatabases.
  private static WeakHashMap<SQLiteDatabase, Object> sActiveDatabases =
    new WeakHashMap<SQLiteDatabase, Object>();

  public int status(int operation, boolean reset){
    return native_status(operation, reset);
  }

  /**
   * Change the password of the open database using sqlite3_rekey().
   *
   * @param password new database password
   *
   * @throws SQLiteException if there is an issue changing the password internally
   *                         OR if the database is not open
   *
   * FUTURE @todo throw IllegalStateException if the database is not open and
   *              update the test suite
   */
  public void changePassword(String password) throws SQLiteException {
    /* safeguard: */
    if (!isOpen()) {
      throw new SQLiteException("database not open");
    }
    if (password != null) {
      byte[] keyMaterial = getBytes(password.toCharArray());
      rekey(keyMaterial);
      Arrays.fill(keyMaterial, (byte) 0);
    }
  }

  /**
   * Change the password of the open database using sqlite3_rekey().
   *
   * @param password new database password (char array)
   *
   * @throws SQLiteException if there is an issue changing the password internally
   *                         OR if the database is not open
   *
   * FUTURE @todo throw IllegalStateException if the database is not open and
   *              update the test suite
   */
  public void changePassword(char[] password) throws SQLiteException {
    /* safeguard: */
    if (!isOpen()) {
      throw new SQLiteException("database not open");
    }
    if (password != null) {
      byte[] keyMaterial = getBytes(password);
      rekey(keyMaterial);
      Arrays.fill(keyMaterial, (byte) 0);
    }
  }

  private static void loadICUData(Context context, File workingDir) {
    OutputStream out = null;
    ZipInputStream in = null;
    File icuDir = new File(workingDir, "icu");
    File icuDataFile = new File(icuDir, "icudt46l.dat");
    try {
      if(!icuDir.exists()) icuDir.mkdirs();
      if(!icuDataFile.exists()) {
        in = new ZipInputStream(context.getAssets().open("icudt46l.zip"));
        in.getNextEntry();
        out =  new FileOutputStream(icuDataFile);
        byte[] buf = new byte[1024];
        int len;
        while ((len = in.read(buf)) > 0) {
          out.write(buf, 0, len);
        }
      }
    }
    catch (Exception ex) {
      if(BuildConfig.DEBUG){
        Log.e(TAG, "Error copying icu dat file", ex);
      }
      if(icuDataFile.exists()){
        icuDataFile.delete();
      }
      throw new RuntimeException(ex);
    }
    finally {
      try {
        if(in != null){
          in.close();
        }
        if(out != null){
          out.flush();
          out.close();
        }
      } catch (IOException ioe){
        if(BuildConfig.DEBUG){
          Log.e(TAG, "Error in closing streams IO streams after expanding ICU dat file", ioe);
        }
        throw new RuntimeException(ioe);
      }
    }
  }

  /**
   * Implement this interface to provide custom strategy for loading jni libraries.
   */
  public interface LibraryLoader {
    /**
     * Load jni libraries by given names.
     * Straightforward implementation will be calling {@link System#loadLibrary(String name)}
     * for every provided library name.
     *
     * @param libNames library names that sqlcipher need to load
     */
    void loadLibraries(String... libNames);
  }

  /**
   * Loads the native SQLCipher library into the application process.
   */
  public static synchronized void loadLibs (Context context) {
    loadLibs(context, context.getFilesDir());
  }

  /**
   * Loads the native SQLCipher library into the application process.
   */
  public static synchronized void loadLibs (Context context, File workingDir) {
    loadLibs(context, workingDir, new LibraryLoader() {
        @Override
        public void loadLibraries(String... libNames) {
          for (String libName : libNames) {
            System.loadLibrary(libName);
          }
        }
      });
  }

  /**
   * Loads the native SQLCipher library into the application process.
   */
  public static synchronized void loadLibs(Context context, LibraryLoader libraryLoader) {
    loadLibs(context, context.getFilesDir(), libraryLoader);
  }

  /**
   * Loads the native SQLCipher library into the application process.
   */
  public static synchronized void loadLibs (Context context, File workingDir, LibraryLoader libraryLoader) {
    libraryLoader.loadLibraries("sqlcipher");

    // 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

  /**
   * SQLite memory database name
   */
  public static final String MEMORY = ":memory:";

  /**
   * 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 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 */ long 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;

  /** {@link DatabaseErrorHandler} to be used when SQLite returns any of the following errors
   *    Corruption
   * */
  private final DatabaseErrorHandler mErrorHandler;

  /** 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();

      synchronized (sActiveDatabases) {
        sActiveDatabases.remove(this);
      }
    }
  }

  /**
   * 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() {
    if(BuildConfig.DEBUG){
      Log.e(TAG, "Calling error handler for corrupt database (detected) " + mPath);
    }

    // NOTE: DefaultDatabaseErrorHandler deletes the corrupt file, EXCEPT for memory database
    mErrorHandler.onCorruption(this);
  }

  /**
   * 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) {
          if(BuildConfig.DEBUG){
            Log.d(TAG, msg, new Exception());
          }
        } else {
          if(BuildConfig.DEBUG){
            Log.d(TAG, msg);
          }
        }
      }
    }
  }

  /**
   * Performs a PRAGMA integrity_check; command against the database.
   * @return true if the integrity check is ok, otherwise false
   */
  public boolean isDatabaseIntegrityOk() {
    Pair<Boolean, String> result = getResultFromPragma("PRAGMA integrity_check;");
    return result.first ? result.second.equals("ok") : result.first;
  }

  /**
   * Returns a list of attached databases including the main database
   * by executing PRAGMA database_list
   * @return a list of pairs of database name and filename
   */
  public List<Pair<String, String>> getAttachedDbs() {
    return getAttachedDbs(this);
  }

  /**
   * Sets the journal mode of the database to WAL
   * @return true if successful, false otherwise
   */
  public boolean enableWriteAheadLogging() {
    if(inTransaction()) {
      String message = "Write Ahead Logging cannot be enabled while in a transaction";
      throw new IllegalStateException(message);
    }
    List<Pair<String, String>> attachedDbs = getAttachedDbs(this);
    if(attachedDbs != null && attachedDbs.size() > 1) return false;
    if(isReadOnly() || getPath().equals(MEMORY)) return false;
    String command = "PRAGMA journal_mode = WAL;";
    rawExecSQL(command);
    return true;
  }

  /**
   * Sets the journal mode of the database to DELETE (the default mode)
   */
  public void disableWriteAheadLogging() {
    if(inTransaction()) {
      String message = "Write Ahead Logging cannot be disabled while in a transaction";
      throw new IllegalStateException(message);
    }
    String command = "PRAGMA journal_mode = DELETE;";
    rawExecSQL(command);
  }

  /**
   * @return true if the journal mode is set to WAL, otherwise false
   */
  public boolean isWriteAheadLoggingEnabled() {
    Pair<Boolean, String> result = getResultFromPragma("PRAGMA journal_mode;");
    return result.first ? result.second.equals("wal") : result.first;
  }

  /**
   * Enables or disables foreign key constraints
   * @param enable used to determine whether or not foreign key constraints are on
   */
  public void setForeignKeyConstraintsEnabled(boolean enable) {
    if(inTransaction()) {
      String message = "Foreign key constraints may not be changed while in a transaction";
      throw new IllegalStateException(message);
    }
    String command = String.format("PRAGMA foreign_keys = %s;",
                                   enable ? "ON" : "OFF");
    execSQL(command);
  }

  /**
   * 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>
   *
   * @throws IllegalStateException if the database is not open
   */
  public void beginTransaction() {
    beginTransactionWithListener(null /* transactionStatusCallback */);
  }

  /**
   * Begins a transaction in Exlcusive mode. 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}.
   *
   * @throws IllegalStateException if the database is not open
   */
  public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) {
    beginTransactionWithListenerInternal(transactionListener,
                                         SQLiteDatabaseTransactionType.Exclusive);
  }

  /**
   * Begins a transaction in Immediate mode
   */
  public void beginTransactionNonExclusive() {
    beginTransactionWithListenerInternal(null,
                                         SQLiteDatabaseTransactionType.Immediate);
  }

  /**
   * Begins a transaction in Immediate mode
   * @param transactionListener is the listener used to report transaction events
   */
  public void beginTransactionWithListenerNonExclusive(SQLiteTransactionListener transactionListener) {
    beginTransactionWithListenerInternal(transactionListener,
                                         SQLiteDatabaseTransactionType.Immediate);
  }

  /**
   * End a transaction. See beginTransaction for notes about how to use this and when transactions
   * are committed and rolled back.
   *
   * @throws IllegalStateException if the database is not open or is not locked by the current thread
   */
  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(BuildConfig.DEBUG){
            Log.d(TAG, "exception during rollback, maybe the DB previously "
                  + "performed an auto-rollback");
          }
        }
      }
    } finally {
      mTransactionListener = null;
      unlockForced();
      if(BuildConfig.DEBUG){
        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 database is not open, 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 transaction was yielded, false if queue was empty or database was not open
   */
  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() {
    /* safeguard: */
    if (!isOpen()) return false;

    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, false if queue was empty or database was not open
   */
  public boolean yieldIfContendedSafely() {
    /* safeguard: */
    if (!isOpen()) return false;

    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, false if queue was empty or database was not open
   *
   * @throws IllegalStateException if the database is locked more than once by the current thread
   * @throws InterruptedException if the thread was interrupted while sleeping
   */
  public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) {
    /* safeguard: */
    if (!isOpen()) return false;

    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 password to use to open and/or create database file
   * @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 and other options
   *
   * @return the newly opened database
   *
   * @throws SQLiteException if the database cannot be opened
   * @throws IllegalArgumentException if the database path is null
   */
  public static SQLiteDatabase openDatabase(String path, String password, CursorFactory factory, int flags) {
    return openDatabase(path, password, factory, flags, null);
  }

  /**
   * 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 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 password to use to open and/or create database file (char array)
   * @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 and other options
   *
   * @return the newly opened database
   *
   * @throws SQLiteException if the database cannot be opened
   * @throws IllegalArgumentException if the database path is null
   */
  public static SQLiteDatabase openDatabase(String path, char[] password, CursorFactory factory, int flags) {
    return openDatabase(path, password, factory, flags, null, null);
  }

  /**
   * Open the database according to the flags {@link #OPEN_READWRITE}
   * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}
   * with optional hook to run on pre/post key events.
   *
   * <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 password to use to open and/or create database file
   * @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 and other options
   * @param hook to run on pre/post key events
   *
   * @return the newly opened database
   *
   * @throws SQLiteException if the database cannot be opened
   * @throws IllegalArgumentException if the database path is null
   */
  public static SQLiteDatabase openDatabase(String path, String password, CursorFactory factory, int flags, SQLiteDatabaseHook hook) {
    return openDatabase(path, password, factory, flags, hook, null);
  }

  /**
   * Open the database according to the flags {@link #OPEN_READWRITE}
   * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}
   * with optional hook to run on pre/post key events.
   *
   * <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 password to use to open and/or create database file (char array)
   * @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 and other options
   * @param hook to run on pre/post key events (may be null)
   *
   * @return the newly opened database
   *
   * @throws SQLiteException if the database cannot be opened
   * @throws IllegalArgumentException if the database path is null
   */
  public static SQLiteDatabase openDatabase(String path, char[] password, CursorFactory factory, int flags, SQLiteDatabaseHook hook) {
    return openDatabase(path, password, factory, flags, hook, null);
  }

  /**
   * Open the database according to the flags {@link #OPEN_READWRITE}
   * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}
   * with optional hook to run on pre/post key events.
   *
   * <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 password to use to open and/or create database file
   * @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 and other options
   * @param hook to run on pre/post key events
   * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite reports database
   * corruption (or null for default).
   *
   * @return the newly opened database
   *
   * @throws SQLiteException if the database cannot be opened
   * @throws IllegalArgumentException if the database path is null
   */
  public static SQLiteDatabase openDatabase(String path, String password, CursorFactory factory, int flags,
                                            SQLiteDatabaseHook hook, DatabaseErrorHandler errorHandler) {
    return openDatabase(path, password == null ? null : password.toCharArray(), factory, flags, hook, errorHandler);
  }

/**
   * Open the database according to the flags {@link #OPEN_READWRITE}
   * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}
   * with optional hook to run on pre/post key events.
   *
   * <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 password to use to open and/or create database file (char array)
   * @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 and other options
   * @param hook to run on pre/post key events (may be null)
   * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite reports database
   * corruption (or null for default).
   *
   * @return the newly opened database
   *
   * @throws SQLiteException if the database cannot be opened
   * @throws IllegalArgumentException if the database path is null
   */
  public static SQLiteDatabase openDatabase(String path, char[] password, CursorFactory factory, int flags,
                                            SQLiteDatabaseHook hook, DatabaseErrorHandler errorHandler) {
    byte[] keyMaterial = getBytes(password);
    return openDatabase(path, keyMaterial, factory, flags, hook, errorHandler);
  }

  /**
   * Open the database according to the flags {@link #OPEN_READWRITE}
   * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}
   * with optional hook to run on pre/post key events.
   *
   * <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 password to use to open and/or create database file (byte array)
   * @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 and other options
   * @param hook to run on pre/post key events (may be null)
   * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite reports database
   * corruption (or null for default).
   *
   * @return the newly opened database
   *
   * @throws SQLiteException if the database cannot be opened
   * @throws IllegalArgumentException if the database path is null
   */
  public static SQLiteDatabase openDatabase(String path, byte[] password, CursorFactory factory, int flags,
                                            SQLiteDatabaseHook hook, DatabaseErrorHandler errorHandler) {
    SQLiteDatabase sqliteDatabase = null;
    DatabaseErrorHandler myErrorHandler = (errorHandler != null) ? errorHandler : new DefaultDatabaseErrorHandler();

    try {
      // Open the database.
      sqliteDatabase = new SQLiteDatabase(path, factory, flags, myErrorHandler);
      sqliteDatabase.openDatabaseInternal(password, hook);
    } catch (SQLiteDatabaseCorruptException e) {
      // Try to recover from this, if possible.
      // FUTURE TBD: should we consider this for other open failures?

      if(BuildConfig.DEBUG){
        Log.e(TAG, "Calling error handler for corrupt database " + path, e);
      }

      // NOTE: if this errorHandler.onCorruption() throws the exception _should_
      // bubble back to the original caller.
      // DefaultDatabaseErrorHandler deletes the corrupt file, EXCEPT for memory database
      myErrorHandler.onCorruption(sqliteDatabase);

      // try *once* again:
      sqliteDatabase = new SQLiteDatabase(path, factory, flags, myErrorHandler);
      sqliteDatabase.openDatabaseInternal(password, hook);
    }

    if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
      sqliteDatabase.enableSqlTracing(path);
    }
    if (SQLiteDebug.DEBUG_SQL_TIME) {
      sqliteDatabase.enableSqlProfiling(path);
    }

    synchronized (sActiveDatabases) {
      sActiveDatabases.put(sqliteDatabase, null);
    }

    return sqliteDatabase;
  }

  /**
   * Equivalent to openDatabase(file.getPath(), password, factory, CREATE_IF_NECESSARY, databaseHook).
   */
  public static SQLiteDatabase openOrCreateDatabase(File file, String password, CursorFactory factory, SQLiteDatabaseHook databaseHook) {
    return openOrCreateDatabase(file, password, factory, databaseHook, null);
  }

  /**
   * Equivalent to openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook).
   */
  public static SQLiteDatabase openOrCreateDatabase(File file, String password, CursorFactory factory, SQLiteDatabaseHook databaseHook,
                                                    DatabaseErrorHandler errorHandler) {
    return openOrCreateDatabase(file == null ? null : file.getPath(), password, factory, databaseHook, errorHandler);
  }

  /**
   * Equivalent to openDatabase(path, password, factory, CREATE_IF_NECESSARY, 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, String password, CursorFactory factory, SQLiteDatabaseHook databaseHook,
                                                    DatabaseErrorHandler errorHandler) {
    return openDatabase(path, password == null ? null : password.toCharArray(), factory, CREATE_IF_NECESSARY, databaseHook, errorHandler);
  }

  public static SQLiteDatabase openOrCreateDatabase(String path, char[] 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,
                                                    DatabaseErrorHandler errorHandler) {
    return openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook, errorHandler);
  }

  public static SQLiteDatabase openOrCreateDatabase(String path, byte[] password, CursorFactory factory, SQLiteDatabaseHook databaseHook) {
    return openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook, null);
  }

  public static SQLiteDatabase openOrCreateDatabase(String path, byte[] password, CursorFactory factory, SQLiteDatabaseHook databaseHook,
                                                    DatabaseErrorHandler errorHandler) {
    return openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook, errorHandler);
  }

  /**
   * Equivalent to openDatabase(file.getPath(), password, factory, CREATE_IF_NECESSARY).
   */
  public static SQLiteDatabase openOrCreateDatabase(File file, String password, CursorFactory factory) {
    return openOrCreateDatabase(file, password, factory, null);
  }

  /**
   * Equivalent to openDatabase(path, password, factory, CREATE_IF_NECESSARY).
   */
  public static SQLiteDatabase openOrCreateDatabase(String path, String password, CursorFactory factory) {
    return openDatabase(path, password, factory, CREATE_IF_NECESSARY, null);
  }

  /**
   * Equivalent to openDatabase(path, password, factory, CREATE_IF_NECESSARY).
   */
  public static SQLiteDatabase openOrCreateDatabase(String path, char[] password, CursorFactory factory) {
    return openDatabase(path, password, factory, CREATE_IF_NECESSARY, null);
  }

  /**
   * Equivalent to openDatabase(path, password, factory, CREATE_IF_NECESSARY).
   */
  public static SQLiteDatabase openOrCreateDatabase(String path, byte[] password, CursorFactory factory) {
    return openDatabase(path, password, factory, CREATE_IF_NECESSARY, null, 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
   * @param password to use to open and/or create database file
   *
   * @return a SQLiteDatabase object, or null if the database can't be created
   *
   * @throws SQLiteException if the database cannot be opened
   */
  public static SQLiteDatabase create(CursorFactory factory, String password) {
    // This is a magic string with special meaning for SQLite.
    return openDatabase(MEMORY, password == null ? null : password.toCharArray(), factory, CREATE_IF_NECESSARY);
  }

  /**
   * 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
   * @param password to use to open and/or create database file (char array)
   *
   * @return a SQLiteDatabase object, or null if the database can't be created
   *
   * @throws SQLiteException if the database cannot be opened
   */
  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
   *
   * @throws IllegalStateException if the database is not open
   */
  public int getVersion() {
    SQLiteStatement prog = null;
    lock();
    try {
      if (!isOpen()) {
        throw new IllegalStateException("database not open");
      }
      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
   *
   * @throws SQLiteException if there is an issue executing the sql internally
   * @throws IllegalStateException if the database is not open
   */
  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();
    try {
      if (!isOpen()) {
        throw new IllegalStateException("database not open");
      }
      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();
    try {
      if (!isOpen()) {
        throw new IllegalStateException("database not open");
      }
      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();
    try {
      if (!isOpen()) {
        throw new IllegalStateException("database not open");
      }
      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
     *
     * @throws SQLiteException if there is an issue executing the sql to mark the table as syncable
     *                         OR if the database is not open
     *
     * FUTURE @todo throw IllegalStateException if the database is not open and
     *              update the test suite
     *
     * NOTE: This method was deprecated by the AOSP in Android API 11.
     */
    public void markTableSyncable(String table, String deletedTable) {
        /* safeguard: */
        if (!isOpen()) {
            throw new SQLiteException("database not open");
        }

        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
     *
     * @throws SQLiteException if there is an issue executing the sql to mark the table as syncable
     *
     * FUTURE @todo throw IllegalStateException if the database is not open and
     *              update the test suite
     *
     * NOTE: This method was deprecated by the AOSP in Android API 11.
     */
    public void markTableSyncable(String table, String foreignKey,
                                  String updateTable) {
        /* safeguard: */
        if (!isOpen()) {
            throw new SQLiteException("database not open");
        }

        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
     *
     * @throws SQLiteException if there is an issue executing the sql
     */
    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.
     *
     * @throws SQLException If the SQL string is invalid for some reason
     * @throws IllegalStateException if the database is not open
     */
    public SQLiteStatement compileStatement(String sql) throws SQLException {
        lock();
        try {
            if (!isOpen()) {
                throw new IllegalStateException("database not open");
            }
            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.
     *
     * @throws SQLiteException if there is an issue executing the sql or the SQL string is invalid
     * @throws IllegalStateException if the database is not open
     *
     * @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.
     *
     * @throws SQLiteException if there is an issue executing the sql or the SQL string is invalid
     * @throws IllegalStateException if the database is not open
     *
     * @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.
     *
     * @throws SQLiteException if there is an issue executing the sql or the SQL string is invalid
     * @throws IllegalStateException if the database is not open
     *
     * @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.
     *
     * @throws SQLiteException if there is an issue executing the sql or the SQL string is invalid
     * @throws IllegalStateException if the database is not open
     */
    public Cursor rawQuery(String sql, String[] selectionArgs) {
        return rawQueryWithFactory(null, sql, selectionArgs, null);
    }

    /**
     * Determines the total size in bytes of the query results, and the largest
     * single row in bytes for the query.
     *
     * @param sql the SQL query. The SQL string must a SELECT statement
     * @param args the argments to bind to the query
     *
     * @return A {@link SQLiteQueryStats} based the provided SQL query.
     */
    public SQLiteQueryStats getQueryStats(String sql, Object[] args){
        long totalPayload = 0L;
        long largestIndividualPayload = 0L;
        try {
            String query = String.format("CREATE TABLE tempstat AS %s", sql);
            execSQL(query, args);
            Cursor cursor = rawQuery("SELECT sum(payload) FROM dbstat WHERE name = 'tempstat';", new Object[]{});
            if(cursor == null) return new SQLiteQueryStats(totalPayload, largestIndividualPayload);
            cursor.moveToFirst();
            totalPayload = cursor.getLong(0);
            cursor.close();
            cursor = rawQuery("SELECT max(mx_payload) FROM dbstat WHERE name = 'tempstat';", new Object[]{});
            if(cursor == null) return new SQLiteQueryStats(totalPayload, largestIndividualPayload);
            cursor.moveToFirst();
            largestIndividualPayload = cursor.getLong(0);
            cursor.close();
            execSQL("DROP TABLE tempstat;");
        } catch(SQLiteException ex) {
            execSQL("DROP TABLE IF EXISTS tempstat;");
            throw ex;
        }
        return new SQLiteQueryStats(totalPayload, largestIndividualPayload);
    }

    /**
     * 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 args You may include ?s in where clause in the query,
     *     which will be replaced by the values from args. The
     *     values will be bound by their type.
     *
     * @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.
     *
     * @throws SQLiteException if there is an issue executing the sql or the SQL string is invalid
     * @throws IllegalStateException if the database is not open
     */
    public Cursor rawQuery(String sql, Object[] args) {
        if (!isOpen()) {
            throw new IllegalStateException("database not open");
        }
        long timeStart = 0;
        if (Config.LOGV || mSlowQueryThreshold != -1) {
            timeStart = System.currentTimeMillis();
        }
        SQLiteDirectCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, null);
        Cursor cursor = null;
        try {
            cursor = driver.query(mFactory, args);
        } finally {
            if (Config.LOGV || mSlowQueryThreshold != -1) {
                // Force query execution
                int count = -1;
                if (cursor != null) {
                    count = cursor.getCount();
                }

                long duration = System.currentTimeMillis() - timeStart;

                if (BuildConfig.DEBUG || 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.
     *
     * @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.
     *
     * @throws SQLiteException if there is an issue executing the sql or the SQL string is invalid
     * @throws IllegalStateException if the database is not open
     */
    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 (BuildConfig.DEBUG || 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) {
          if(BuildConfig.DEBUG){
            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) {
          if(BuildConfig.DEBUG){
            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
     *
     * @throws SQLException If the SQL string is invalid for some reason
     * @throws IllegalStateException if the database is not open
     */
    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 = lastChangeCount() > 0 ? lastInsertRow() : -1;
            if (insertedRowId == -1) {
              if(BuildConfig.DEBUG){
                Log.e(TAG, "Error inserting <redacted values> using <redacted sql> into " + table);
              }
            } else {
                if (BuildConfig.DEBUG && 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.
     *
     * @throws SQLException If the SQL string is invalid for some reason
     * @throws IllegalStateException if the database is not open
     */
    public int delete(String table, String whereClause, String[] whereArgs) {
        SQLiteStatement statement = null;
        lock();
        try {
            if (!isOpen()) {
                throw new IllegalStateException("database not open");
            }
            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
     *
     * @throws SQLException If the SQL string is invalid for some reason
     * @throws IllegalStateException if the database is not open
     */
    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
     *
     * @throws SQLException If the SQL string is invalid for some reason
     * @throws IllegalStateException if the database is not open
     */
    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);
        }
        SQLiteStatement statement = null;
        lock();
        try {
            if (!isOpen()) {
                throw new IllegalStateException("database not open");
            }
            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 (BuildConfig.DEBUG && 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) {
          if(BuildConfig.DEBUG){
            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
     * @throws IllegalStateException if the database is not open
     */
    public void execSQL(String sql) throws SQLException {
        long timeStart = SystemClock.uptimeMillis();
        lock();
        try {
            if (!isOpen()) {
                throw new IllegalStateException("database not open");
            }
            native_execSQL(sql);
        } catch (SQLiteDatabaseCorruptException e) {
            onCorruption();
            throw e;
        } finally {
            unlock();
        }
    }

    public void rawExecSQL(String sql){
        long timeStart = SystemClock.uptimeMillis();
        lock();
        try {
            if (!isOpen()) {
                throw new IllegalStateException("database not open");
            }
            native_rawExecSQL(sql);
        } catch (SQLiteDatabaseCorruptException e) {
            onCorruption();
            throw e;
        } finally {
            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,
     *
     * @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
     * @throws IllegalStateException if the database is not open
     */
    public void execSQL(String sql, Object[] bindArgs) throws SQLException {
        SQLiteStatement statement = null;
        if (bindArgs == null) {
            throw new IllegalArgumentException("Empty bindArgs");
        }
        long timeStart = SystemClock.uptimeMillis();
        lock();
        try {
            if (!isOpen()) {
                throw new IllegalStateException("database not open");
            }
            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();
        }
    }

    @Override
    protected void finalize() {
        if (isOpen()) {
          if(BuildConfig.DEBUG){
            Log.e(TAG, "close() was never explicitly called on database '" +
                  mPath + "' ", mStackTrace);
          }
          closeClosable();
          onAllReferencesReleased();
        }
    }

    /**
     * Public constructor which attempts to open the database. See {@link #create} and {@link #openDatabase}.
     *
     * <p>Sets the locale of the database to the system's current locale.
     * Call {@link #setLocale} if you would like something else.</p>
     *
     * @param path The full path to the database
     * @param password to use to open and/or create a database file (char array)
     * @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.
     *
     * @throws SQLiteException if the database cannot be opened
     * @throws IllegalArgumentException if the database path is null
     */
    public SQLiteDatabase(String path, char[] password, CursorFactory factory, int flags) {
        this(path, factory, flags, null);
        this.openDatabaseInternal(password, null);
    }

    /**
     * Public constructor which attempts to open the database. See {@link #create} and {@link #openDatabase}.
     *
     * <p>Sets the locale of the database to the system's current locale.
     * Call {@link #setLocale} if you would like something else.</p>
     *
     * @param path The full path to the database
     * @param password to use to open and/or create a database file (char array)
     * @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.
     * @param databaseHook to run on pre/post key events
     *
     * @throws SQLiteException if the database cannot be opened
     * @throws IllegalArgumentException if the database path is null
     */
    public SQLiteDatabase(String path, char[] password, CursorFactory factory, int flags, SQLiteDatabaseHook databaseHook) {
        this(path, factory, flags, null);
        this.openDatabaseInternal(password, databaseHook);
    }

    public SQLiteDatabase(String path, byte[] password, CursorFactory factory, int flags, SQLiteDatabaseHook databaseHook) {
        this(path, factory, flags, null);
        this.openDatabaseInternal(password, databaseHook);
    }

    /**
     * Private constructor (without database password) which DOES NOT attempt to open the database.
     *
     * @param path The full path to the database
     * @param factory The factory to use when creating cursors, may be NULL.
     * @param flags to control database access mode and other options
     * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite reports database
     * corruption (or null for default).
     *
     * @throws IllegalArgumentException if the database path is null
     */
    private SQLiteDatabase(String path, CursorFactory factory, int flags, DatabaseErrorHandler errorHandler) {
        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>();

        mErrorHandler = errorHandler;
    }

    private void openDatabaseInternal(final char[] password, SQLiteDatabaseHook hook) {
        final byte[] keyMaterial = getBytes(password);
        openDatabaseInternal(keyMaterial, hook);
    }

    private void openDatabaseInternal(final byte[] password, SQLiteDatabaseHook hook) {
        boolean shouldCloseConnection = true;
        dbopen(mPath, mFlags);
        try {
            keyDatabase(hook, new Runnable() {
                    public void run() {
                        if(password != null && password.length > 0) {
                            key(password);
                        }
                    }
                });
            shouldCloseConnection = false;

        } catch(RuntimeException ex) {

            final char[] keyMaterial = getChars(password);
            if(containsNull(keyMaterial)) {
                keyDatabase(hook, new Runnable() {
                        public void run() {
                            if(password != null) {
                                key_mutf8(keyMaterial);
                            }
                        }
                    });
                if(password != null && password.length > 0) {
                    rekey(password);
                }
                shouldCloseConnection = false;
            } else {
                throw ex;
            }
            if(keyMaterial != null && keyMaterial.length > 0) {
                Arrays.fill(keyMaterial, (char)0);
            }

        } finally {
            if(shouldCloseConnection) {
                dbclose();
                if (SQLiteDebug.DEBUG_SQL_CACHE) {
                    mTimeClosed = getTime();
                }
            }
        }

    }

    private boolean containsNull(char[] data) {
        char defaultValue = '\u0000';
        boolean status = false;
        if(data != null && data.length > 0) {
            for(char datum : data) {
                if(datum == defaultValue) {
                    status = true;
                    break;
                }
            }
        }
        return status;
    }

    private void keyDatabase(SQLiteDatabaseHook databaseHook, Runnable keyOperation) {
        if(databaseHook != null) {
            databaseHook.preKey(this);
        }
        if(keyOperation != null){
            keyOperation.run();
        }
        if(databaseHook != null){
            databaseHook.postKey(this);
        }
        if (SQLiteDebug.DEBUG_SQL_CACHE) {
            mTimeOpened = getTime();
        }
        try {
            Cursor cursor = rawQuery("select count(*) from sqlite_master;", new String[]{});
            if(cursor != null){
                cursor.moveToFirst();
                int count = cursor.getInt(0);
                cursor.close();
            }
        } catch (RuntimeException e) {
          if(BuildConfig.DEBUG){
            Log.e(TAG, e.getMessage(), e);
          }
          throw e;
        }
    }

    private String getTime() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS ", Locale.US).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) {
        /* NOTE: getVersion() will throw if database is not open. */
        return newVersion > getVersion();
    }

    /**
     * Getter for the path to the database file.
     *
     * @return the path to our database file.
     */
    public final String getPath() {
        return mPath;
    }

    /**
     * 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 && BuildConfig.DEBUG) {
                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 && BuildConfig.DEBUG) {
                    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 && BuildConfig.DEBUG) {
                    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 && BuildConfig.DEBUG) {
                    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 && BuildConfig.DEBUG) {
            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;
    }

    public static byte[] getBytes(char[] data) {
        if(data == null || data.length == 0) return null;
        CharBuffer charBuffer = CharBuffer.wrap(data);
        ByteBuffer byteBuffer = Charset.forName(KEY_ENCODING).encode(charBuffer);
        byte[] result =  new byte[byteBuffer.limit()];
        byteBuffer.get(result);
        return result;
    }

    public static char[] getChars(byte[] data){
        if(data == null || data.length == 0) return null;
        ByteBuffer byteBuffer = ByteBuffer.wrap(data);
        CharBuffer charBuffer = Charset.forName(KEY_ENCODING).decode(byteBuffer);
        char[] result = new char[charBuffer.limit()];
        charBuffer.get(result);
        return result;
    }

    private void beginTransactionWithListenerInternal(SQLiteTransactionListener transactionListener,
                                                      SQLiteDatabaseTransactionType transactionType) {
        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);
                    if(BuildConfig.DEBUG){
                      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.
            if(transactionType == SQLiteDatabaseTransactionType.Exclusive) {
                execSQL("BEGIN EXCLUSIVE;");
            } else if(transactionType == SQLiteDatabaseTransactionType.Immediate) {
                execSQL("BEGIN IMMEDIATE;");
            } else if(transactionType == SQLiteDatabaseTransactionType.Deferred) {
                execSQL("BEGIN DEFERRED;");
            } else {
                String message = String.format("%s is an unsupported transaction type",
                                               transactionType);
                throw new IllegalArgumentException(message);
            }
            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();
            }
        }
    }

    /**
     * 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 (SQLiteDatabase db : getActiveDatabases()) {
            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;
    }

    private static ArrayList<SQLiteDatabase> getActiveDatabases() {
        ArrayList<SQLiteDatabase> databases = new ArrayList<SQLiteDatabase>();
        synchronized (sActiveDatabases) {
            databases.addAll(sActiveDatabases.keySet());
        }
        return databases;
    }

    /**
     * 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;
    }

    private Pair<Boolean, String> getResultFromPragma(String command) {
        Cursor cursor = rawQuery(command, new Object[]{});
        if(cursor == null) return new Pair(false, "");
        cursor.moveToFirst();
        String value = cursor.getString(0);
        cursor.close();
        return new Pair(true, value);
    }


    /**
     * 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 key(byte[] key) throws SQLException;
    private native void key_mutf8(char[] key) throws SQLException;
    private native void rekey(byte[] key) throws SQLException;
}
