Automatic sources dropoff on 2020-06-10 18:32:38.095721

The change is generated with prebuilt drop tool.

Change-Id: I24cbf6ba6db262a1ae1445db1427a08fee35b3b4
diff --git a/android/database/AbstractCursor.java b/android/database/AbstractCursor.java
new file mode 100644
index 0000000..3effc5a
--- /dev/null
+++ b/android/database/AbstractCursor.java
@@ -0,0 +1,552 @@
+/*
+ * 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 android.database;
+
+import android.annotation.NonNull;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+
+/**
+ * This is an abstract cursor class that handles a lot of the common code
+ * that all cursors need to deal with and is provided for convenience reasons.
+ */
+public abstract class AbstractCursor implements CrossProcessCursor {
+    private static final String TAG = "Cursor";
+
+    /**
+     * @removed This field should not be used.
+     */
+    protected HashMap<Long, Map<String, Object>> mUpdatedRows;
+
+    /**
+     * @removed This field should not be used.
+     */
+    protected int mRowIdColumnIndex;
+
+    /**
+     * @removed This field should not be used.
+     */
+    protected Long mCurrentRowID;
+
+    /**
+     * @deprecated Use {@link #getPosition()} instead.
+     */
+    @Deprecated
+    protected int mPos;
+
+    /**
+     * @deprecated Use {@link #isClosed()} instead.
+     */
+    @Deprecated
+    protected boolean mClosed;
+
+    /**
+     * @deprecated Do not use.
+     */
+    @Deprecated
+    protected ContentResolver mContentResolver;
+
+    @UnsupportedAppUsage
+    private Uri mNotifyUri;
+    private List<Uri> mNotifyUris;
+
+    private final Object mSelfObserverLock = new Object();
+    private ContentObserver mSelfObserver;
+    private boolean mSelfObserverRegistered;
+
+    private final DataSetObservable mDataSetObservable = new DataSetObservable();
+    private final ContentObservable mContentObservable = new ContentObservable();
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    private Bundle mExtras = Bundle.EMPTY;
+
+    /* -------------------------------------------------------- */
+    /* These need to be implemented by subclasses */
+    @Override
+    abstract public int getCount();
+
+    @Override
+    abstract public String[] getColumnNames();
+
+    @Override
+    abstract public String getString(int column);
+    @Override
+    abstract public short getShort(int column);
+    @Override
+    abstract public int getInt(int column);
+    @Override
+    abstract public long getLong(int column);
+    @Override
+    abstract public float getFloat(int column);
+    @Override
+    abstract public double getDouble(int column);
+    @Override
+    abstract public boolean isNull(int column);
+
+    @Override
+    public int getType(int column) {
+        // Reflects the assumption that all commonly used field types (meaning everything
+        // but blobs) are convertible to strings so it should be safe to call
+        // getString to retrieve them.
+        return FIELD_TYPE_STRING;
+    }
+
+    // TODO implement getBlob in all cursor types
+    @Override
+    public byte[] getBlob(int column) {
+        throw new UnsupportedOperationException("getBlob is not supported");
+    }
+    /* -------------------------------------------------------- */
+    /* Methods that may optionally be implemented by subclasses */
+
+    /**
+     * If the cursor is backed by a {@link CursorWindow}, returns a pre-filled
+     * window with the contents of the cursor, otherwise null.
+     *
+     * @return The pre-filled window that backs this cursor, or null if none.
+     */
+    @Override
+    public CursorWindow getWindow() {
+        return null;
+    }
+
+    @Override
+    public int getColumnCount() {
+        return getColumnNames().length;
+    }
+
+    @Override
+    public void deactivate() {
+        onDeactivateOrClose();
+    }
+
+    /** @hide */
+    protected void onDeactivateOrClose() {
+        if (mSelfObserver != null) {
+            mContentResolver.unregisterContentObserver(mSelfObserver);
+            mSelfObserverRegistered = false;
+        }
+        mDataSetObservable.notifyInvalidated();
+    }
+
+    @Override
+    public boolean requery() {
+        if (mSelfObserver != null && mSelfObserverRegistered == false) {
+            final int size = mNotifyUris.size();
+            for (int i = 0; i < size; ++i) {
+                final Uri notifyUri = mNotifyUris.get(i);
+                mContentResolver.registerContentObserver(notifyUri, true, mSelfObserver);
+            }
+            mSelfObserverRegistered = true;
+        }
+        mDataSetObservable.notifyChanged();
+        return true;
+    }
+
+    @Override
+    public boolean isClosed() {
+        return mClosed;
+    }
+
+    @Override
+    public void close() {
+        mClosed = true;
+        mContentObservable.unregisterAll();
+        onDeactivateOrClose();
+    }
+
+    /**
+     * This function is called every time the cursor is successfully scrolled
+     * to a new position, giving the subclass a chance to update any state it
+     * may have. If it returns false the move function will also do so and the
+     * cursor will scroll to the beforeFirst position.
+     *
+     * @param oldPosition the position that we're moving from
+     * @param newPosition the position that we're moving to
+     * @return true if the move is successful, false otherwise
+     */
+    @Override
+    public boolean onMove(int oldPosition, int newPosition) {
+        return true;
+    }
+
+
+    @Override
+    public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
+        // Default implementation, uses getString
+        String result = getString(columnIndex);
+        if (result != null) {
+            char[] data = buffer.data;
+            if (data == null || data.length < result.length()) {
+                buffer.data = result.toCharArray();
+            } else {
+                result.getChars(0, result.length(), data, 0);
+            }
+            buffer.sizeCopied = result.length();
+        } else {
+            buffer.sizeCopied = 0;
+        }
+    }
+
+    /* -------------------------------------------------------- */
+    /* Implementation */
+    public AbstractCursor() {
+        mPos = -1;
+    }
+
+    @Override
+    public final int getPosition() {
+        return mPos;
+    }
+
+    @Override
+    public final boolean moveToPosition(int position) {
+        // Make sure position isn't past the end of the cursor
+        final int count = getCount();
+        if (position >= count) {
+            mPos = count;
+            return false;
+        }
+
+        // Make sure position isn't before the beginning of the cursor
+        if (position < 0) {
+            mPos = -1;
+            return false;
+        }
+
+        // Check for no-op moves, and skip the rest of the work for them
+        if (position == mPos) {
+            return true;
+        }
+
+        boolean result = onMove(mPos, position);
+        if (result == false) {
+            mPos = -1;
+        } else {
+            mPos = position;
+        }
+
+        return result;
+    }
+
+    @Override
+    public void fillWindow(int position, CursorWindow window) {
+        DatabaseUtils.cursorFillWindow(this, position, window);
+    }
+
+    @Override
+    public final boolean move(int offset) {
+        return moveToPosition(mPos + offset);
+    }
+
+    @Override
+    public final boolean moveToFirst() {
+        return moveToPosition(0);
+    }
+
+    @Override
+    public final boolean moveToLast() {
+        return moveToPosition(getCount() - 1);
+    }
+
+    @Override
+    public final boolean moveToNext() {
+        return moveToPosition(mPos + 1);
+    }
+
+    @Override
+    public final boolean moveToPrevious() {
+        return moveToPosition(mPos - 1);
+    }
+
+    @Override
+    public final boolean isFirst() {
+        return mPos == 0 && getCount() != 0;
+    }
+
+    @Override
+    public final boolean isLast() {
+        int cnt = getCount();
+        return mPos == (cnt - 1) && cnt != 0;
+    }
+
+    @Override
+    public final boolean isBeforeFirst() {
+        if (getCount() == 0) {
+            return true;
+        }
+        return mPos == -1;
+    }
+
+    @Override
+    public final boolean isAfterLast() {
+        if (getCount() == 0) {
+            return true;
+        }
+        return mPos == getCount();
+    }
+
+    @Override
+    public int getColumnIndex(String columnName) {
+        // Hack according to bug 903852
+        final int periodIndex = columnName.lastIndexOf('.');
+        if (periodIndex != -1) {
+            Exception e = new Exception();
+            Log.e(TAG, "requesting column name with table name -- " + columnName, e);
+            columnName = columnName.substring(periodIndex + 1);
+        }
+
+        String columnNames[] = getColumnNames();
+        int length = columnNames.length;
+        for (int i = 0; i < length; i++) {
+            if (columnNames[i].equalsIgnoreCase(columnName)) {
+                return i;
+            }
+        }
+
+        if (false) {
+            if (getCount() > 0) {
+                Log.w("AbstractCursor", "Unknown column " + columnName);
+            }
+        }
+        return -1;
+    }
+
+    @Override
+    public int getColumnIndexOrThrow(String columnName) {
+        final int index = getColumnIndex(columnName);
+        if (index < 0) {
+            String availableColumns = "";
+            try {
+                availableColumns = Arrays.toString(getColumnNames());
+            } catch (Exception e) {
+                Log.d(TAG, "Cannot collect column names for debug purposes", e);
+            }
+            throw new IllegalArgumentException("column '" + columnName
+                    + "' does not exist. Available columns: " + availableColumns);
+        }
+        return index;
+    }
+
+    @Override
+    public String getColumnName(int columnIndex) {
+        return getColumnNames()[columnIndex];
+    }
+
+    @Override
+    public void registerContentObserver(ContentObserver observer) {
+        mContentObservable.registerObserver(observer);
+    }
+
+    @Override
+    public void unregisterContentObserver(ContentObserver observer) {
+        // cursor will unregister all observers when it close
+        if (!mClosed) {
+            mContentObservable.unregisterObserver(observer);
+        }
+    }
+
+    @Override
+    public void registerDataSetObserver(DataSetObserver observer) {
+        mDataSetObservable.registerObserver(observer);
+    }
+
+    @Override
+    public void unregisterDataSetObserver(DataSetObserver observer) {
+        mDataSetObservable.unregisterObserver(observer);
+    }
+
+    /**
+     * Subclasses must call this method when they finish committing updates to notify all
+     * observers.
+     *
+     * @param selfChange
+     */
+    protected void onChange(boolean selfChange) {
+        synchronized (mSelfObserverLock) {
+            mContentObservable.dispatchChange(selfChange, null);
+            if (mNotifyUris != null && selfChange) {
+                final int size = mNotifyUris.size();
+                for (int i = 0; i < size; ++i) {
+                    final Uri notifyUri = mNotifyUris.get(i);
+                    mContentResolver.notifyChange(notifyUri, mSelfObserver);
+                }
+            }
+        }
+    }
+
+    /**
+     * Specifies a content URI to watch for changes.
+     *
+     * @param cr The content resolver from the caller's context.
+     * @param notifyUri The URI to watch for changes. This can be a
+     * specific row URI, or a base URI for a whole class of content.
+     */
+    @Override
+    public void setNotificationUri(ContentResolver cr, Uri notifyUri) {
+        setNotificationUris(cr, Arrays.asList(notifyUri));
+    }
+
+    @Override
+    public void setNotificationUris(@NonNull ContentResolver cr, @NonNull List<Uri> notifyUris) {
+        Objects.requireNonNull(cr);
+        Objects.requireNonNull(notifyUris);
+
+        setNotificationUris(cr, notifyUris, cr.getUserId(), true);
+    }
+
+    /**
+     * Set the notification uri but with an observer for a particular user's view. Also allows
+     * disabling the use of a self observer, which is sensible if either
+     * a) the cursor's owner calls {@link #onChange(boolean)} whenever the content changes, or
+     * b) the cursor is known not to have any content observers.
+     * @hide
+     */
+    public void setNotificationUris(ContentResolver cr, List<Uri> notifyUris, int userHandle,
+            boolean registerSelfObserver) {
+        synchronized (mSelfObserverLock) {
+            mNotifyUris = notifyUris;
+            mNotifyUri = mNotifyUris.get(0);
+            mContentResolver = cr;
+            if (mSelfObserver != null) {
+                mContentResolver.unregisterContentObserver(mSelfObserver);
+                mSelfObserverRegistered = false;
+            }
+            if (registerSelfObserver) {
+                mSelfObserver = new SelfContentObserver(this);
+                final int size = mNotifyUris.size();
+                for (int i = 0; i < size; ++i) {
+                    final Uri notifyUri = mNotifyUris.get(i);
+                    mContentResolver.registerContentObserver(
+                            notifyUri, true, mSelfObserver, userHandle);
+                }
+                mSelfObserverRegistered = true;
+            }
+        }
+    }
+
+    @Override
+    public Uri getNotificationUri() {
+        synchronized (mSelfObserverLock) {
+            return mNotifyUri;
+        }
+    }
+
+    @Override
+    public List<Uri> getNotificationUris() {
+        synchronized (mSelfObserverLock) {
+            return mNotifyUris;
+        }
+    }
+
+    @Override
+    public boolean getWantsAllOnMoveCalls() {
+        return false;
+    }
+
+    @Override
+    public void setExtras(Bundle extras) {
+        mExtras = (extras == null) ? Bundle.EMPTY : extras;
+    }
+
+    @Override
+    public Bundle getExtras() {
+        return mExtras;
+    }
+
+    @Override
+    public Bundle respond(Bundle extras) {
+        return Bundle.EMPTY;
+    }
+
+    /**
+     * @deprecated Always returns false since Cursors do not support updating rows
+     */
+    @Deprecated
+    protected boolean isFieldUpdated(int columnIndex) {
+        return false;
+    }
+
+    /**
+     * @deprecated Always returns null since Cursors do not support updating rows
+     */
+    @Deprecated
+    protected Object getUpdatedField(int columnIndex) {
+        return null;
+    }
+
+    /**
+     * This function throws CursorIndexOutOfBoundsException if
+     * the cursor position is out of bounds. Subclass implementations of
+     * the get functions should call this before attempting
+     * to retrieve data.
+     *
+     * @throws CursorIndexOutOfBoundsException
+     */
+    protected void checkPosition() {
+        if (-1 == mPos || getCount() == mPos) {
+            throw new CursorIndexOutOfBoundsException(mPos, getCount());
+        }
+    }
+
+    @Override
+    protected void finalize() {
+        if (mSelfObserver != null && mSelfObserverRegistered == true) {
+            mContentResolver.unregisterContentObserver(mSelfObserver);
+        }
+        try {
+            if (!mClosed) close();
+        } catch(Exception e) { }
+    }
+
+    /**
+     * Cursors use this class to track changes others make to their URI.
+     */
+    protected static class SelfContentObserver extends ContentObserver {
+        WeakReference<AbstractCursor> mCursor;
+
+        public SelfContentObserver(AbstractCursor cursor) {
+            super(null);
+            mCursor = new WeakReference<AbstractCursor>(cursor);
+        }
+
+        @Override
+        public boolean deliverSelfNotifications() {
+            return false;
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            AbstractCursor cursor = mCursor.get();
+            if (cursor != null) {
+                cursor.onChange(false);
+            }
+        }
+    }
+}
diff --git a/android/database/AbstractWindowedCursor.java b/android/database/AbstractWindowedCursor.java
new file mode 100644
index 0000000..daf7d2b
--- /dev/null
+++ b/android/database/AbstractWindowedCursor.java
@@ -0,0 +1,215 @@
+/*
+ * 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 android.database;
+
+import android.compat.annotation.UnsupportedAppUsage;
+
+/**
+ * A base class for Cursors that store their data in {@link CursorWindow}s.
+ * <p>
+ * The cursor owns the cursor window it uses.  When the cursor is closed,
+ * its window is also closed.  Likewise, when the window used by the cursor is
+ * changed, its old window is closed.  This policy of strict ownership ensures
+ * that cursor windows are not leaked.
+ * </p><p>
+ * Subclasses are responsible for filling the cursor window with data during
+ * {@link #onMove(int, int)}, allocating a new cursor window if necessary.
+ * During {@link #requery()}, the existing cursor window should be cleared and
+ * filled with new data.
+ * </p><p>
+ * If the contents of the cursor change or become invalid, the old window must be closed
+ * (because it is owned by the cursor) and set to null.
+ * </p>
+ */
+public abstract class AbstractWindowedCursor extends AbstractCursor {
+    /**
+     * The cursor window owned by this cursor.
+     */
+    protected CursorWindow mWindow;
+
+    @Override
+    public byte[] getBlob(int columnIndex) {
+        checkPosition();
+        return mWindow.getBlob(mPos, columnIndex);
+    }
+
+    @Override
+    public String getString(int columnIndex) {
+        checkPosition();
+        return mWindow.getString(mPos, columnIndex);
+    }
+
+    @Override
+    public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
+        checkPosition();
+        mWindow.copyStringToBuffer(mPos, columnIndex, buffer);
+    }
+
+    @Override
+    public short getShort(int columnIndex) {
+        checkPosition();
+        return mWindow.getShort(mPos, columnIndex);
+    }
+
+    @Override
+    public int getInt(int columnIndex) {
+        checkPosition();
+        return mWindow.getInt(mPos, columnIndex);
+    }
+
+    @Override
+    public long getLong(int columnIndex) {
+        checkPosition();
+        return mWindow.getLong(mPos, columnIndex);
+    }
+
+    @Override
+    public float getFloat(int columnIndex) {
+        checkPosition();
+        return mWindow.getFloat(mPos, columnIndex);
+    }
+
+    @Override
+    public double getDouble(int columnIndex) {
+        checkPosition();
+        return mWindow.getDouble(mPos, columnIndex);
+    }
+
+    @Override
+    public boolean isNull(int columnIndex) {
+        checkPosition();
+        return mWindow.getType(mPos, columnIndex) == Cursor.FIELD_TYPE_NULL;
+    }
+
+    /**
+     * @deprecated Use {@link #getType}
+     */
+    @Deprecated
+    public boolean isBlob(int columnIndex) {
+        return getType(columnIndex) == Cursor.FIELD_TYPE_BLOB;
+    }
+
+    /**
+     * @deprecated Use {@link #getType}
+     */
+    @Deprecated
+    public boolean isString(int columnIndex) {
+        return getType(columnIndex) == Cursor.FIELD_TYPE_STRING;
+    }
+
+    /**
+     * @deprecated Use {@link #getType}
+     */
+    @Deprecated
+    public boolean isLong(int columnIndex) {
+        return getType(columnIndex) == Cursor.FIELD_TYPE_INTEGER;
+    }
+
+    /**
+     * @deprecated Use {@link #getType}
+     */
+    @Deprecated
+    public boolean isFloat(int columnIndex) {
+        return getType(columnIndex) == Cursor.FIELD_TYPE_FLOAT;
+    }
+
+    @Override
+    public int getType(int columnIndex) {
+        checkPosition();
+        return mWindow.getType(mPos, columnIndex);
+    }
+
+    @Override
+    protected void checkPosition() {
+        super.checkPosition();
+        
+        if (mWindow == null) {
+            throw new StaleDataException("Attempting to access a closed CursorWindow." +
+                    "Most probable cause: cursor is deactivated prior to calling this method.");
+        }
+    }
+
+    @Override
+    public CursorWindow getWindow() {
+        return mWindow;
+    }
+
+    /**
+     * Sets a new cursor window for the cursor to use.
+     * <p>
+     * The cursor takes ownership of the provided cursor window; the cursor window
+     * will be closed when the cursor is closed or when the cursor adopts a new
+     * cursor window.
+     * </p><p>
+     * If the cursor previously had a cursor window, then it is closed when the
+     * new cursor window is assigned.
+     * </p>
+     *
+     * @param window The new cursor window, typically a remote cursor window.
+     */
+    public void setWindow(CursorWindow window) {
+        if (window != mWindow) {
+            closeWindow();
+            mWindow = window;
+        }
+    }
+
+    /**
+     * Returns true if the cursor has an associated cursor window.
+     *
+     * @return True if the cursor has an associated cursor window.
+     */
+    public boolean hasWindow() {
+        return mWindow != null;
+    }
+
+    /**
+     * Closes the cursor window and sets {@link #mWindow} to null.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    protected void closeWindow() {
+        if (mWindow != null) {
+            mWindow.close();
+            mWindow = null;
+        }
+    }
+
+    /**
+     * If there is a window, clear it.
+     * Otherwise, creates a new window.
+     *
+     * @param name The window name.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    protected void clearOrCreateWindow(String name) {
+        if (mWindow == null) {
+            mWindow = new CursorWindow(name);
+        } else {
+            mWindow.clear();
+        }
+    }
+
+    /** @hide */
+    @Override
+    @UnsupportedAppUsage
+    protected void onDeactivateOrClose() {
+        super.onDeactivateOrClose();
+        closeWindow();
+    }
+}
diff --git a/android/database/BulkCursorDescriptor.java b/android/database/BulkCursorDescriptor.java
new file mode 100644
index 0000000..80a0319
--- /dev/null
+++ b/android/database/BulkCursorDescriptor.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2012 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 android.database;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Describes the properties of a {@link CursorToBulkCursorAdaptor} that are
+ * needed to initialize its {@link BulkCursorToCursorAdaptor} counterpart on the client's end.
+ *
+ * {@hide}
+ */
+public final class BulkCursorDescriptor implements Parcelable {
+    public static final @android.annotation.NonNull Parcelable.Creator<BulkCursorDescriptor> CREATOR =
+            new Parcelable.Creator<BulkCursorDescriptor>() {
+        @Override
+        public BulkCursorDescriptor createFromParcel(Parcel in) {
+            BulkCursorDescriptor d = new BulkCursorDescriptor();
+            d.readFromParcel(in);
+            return d;
+        }
+
+        @Override
+        public BulkCursorDescriptor[] newArray(int size) {
+            return new BulkCursorDescriptor[size];
+        }
+    };
+
+    public IBulkCursor cursor;
+    public String[] columnNames;
+    public boolean wantsAllOnMoveCalls;
+    public int count;
+    public CursorWindow window;
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeStrongBinder(cursor.asBinder());
+        out.writeStringArray(columnNames);
+        out.writeInt(wantsAllOnMoveCalls ? 1 : 0);
+        out.writeInt(count);
+        if (window != null) {
+            out.writeInt(1);
+            window.writeToParcel(out, flags);
+        } else {
+            out.writeInt(0);
+        }
+    }
+
+    public void readFromParcel(Parcel in) {
+        cursor = BulkCursorNative.asInterface(in.readStrongBinder());
+        columnNames = in.readStringArray();
+        wantsAllOnMoveCalls = in.readInt() != 0;
+        count = in.readInt();
+        if (in.readInt() != 0) {
+            window = CursorWindow.CREATOR.createFromParcel(in);
+        }
+    }
+}
diff --git a/android/database/BulkCursorNative.java b/android/database/BulkCursorNative.java
new file mode 100644
index 0000000..8ea450c
--- /dev/null
+++ b/android/database/BulkCursorNative.java
@@ -0,0 +1,285 @@
+/*
+ * 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 android.database;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+
+/**
+ * Native implementation of the bulk cursor. This is only for use in implementing
+ * IPC, application code should use the Cursor interface.
+ *
+ * {@hide}
+ */
+public abstract class BulkCursorNative extends Binder implements IBulkCursor
+{
+    public BulkCursorNative()
+    {
+        attachInterface(this, descriptor);
+    }
+
+    /**
+     * Cast a Binder object into a content resolver interface, generating
+     * a proxy if needed.
+     */
+    static public IBulkCursor asInterface(IBinder obj)
+    {
+        if (obj == null) {
+            return null;
+        }
+        IBulkCursor in = (IBulkCursor)obj.queryLocalInterface(descriptor);
+        if (in != null) {
+            return in;
+        }
+
+        return new BulkCursorProxy(obj);
+    }
+    
+    @Override
+    public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+            throws RemoteException {
+        try {
+            switch (code) {
+                case GET_CURSOR_WINDOW_TRANSACTION: {
+                    data.enforceInterface(IBulkCursor.descriptor);
+                    int startPos = data.readInt();
+                    CursorWindow window = getWindow(startPos);
+                    reply.writeNoException();
+                    if (window == null) {
+                        reply.writeInt(0);
+                    } else {
+                        reply.writeInt(1);
+                        window.writeToParcel(reply, Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+                    }
+                    return true;
+                }
+
+                case DEACTIVATE_TRANSACTION: {
+                    data.enforceInterface(IBulkCursor.descriptor);
+                    deactivate();
+                    reply.writeNoException();
+                    return true;
+                }
+                
+                case CLOSE_TRANSACTION: {
+                    data.enforceInterface(IBulkCursor.descriptor);
+                    close();
+                    reply.writeNoException();
+                    return true;
+                }
+
+                case REQUERY_TRANSACTION: {
+                    data.enforceInterface(IBulkCursor.descriptor);
+                    IContentObserver observer =
+                            IContentObserver.Stub.asInterface(data.readStrongBinder());
+                    int count = requery(observer);
+                    reply.writeNoException();
+                    reply.writeInt(count);
+                    reply.writeBundle(getExtras());
+                    return true;
+                }
+
+                case ON_MOVE_TRANSACTION: {
+                    data.enforceInterface(IBulkCursor.descriptor);
+                    int position = data.readInt();
+                    onMove(position);
+                    reply.writeNoException();
+                    return true;
+                }
+
+                case GET_EXTRAS_TRANSACTION: {
+                    data.enforceInterface(IBulkCursor.descriptor);
+                    Bundle extras = getExtras();
+                    reply.writeNoException();
+                    reply.writeBundle(extras);
+                    return true;
+                }
+
+                case RESPOND_TRANSACTION: {
+                    data.enforceInterface(IBulkCursor.descriptor);
+                    Bundle extras = data.readBundle();
+                    Bundle returnExtras = respond(extras);
+                    reply.writeNoException();
+                    reply.writeBundle(returnExtras);
+                    return true;
+                }
+            }
+        } catch (Exception e) {
+            DatabaseUtils.writeExceptionToParcel(reply, e);
+            return true;
+        }
+
+        return super.onTransact(code, data, reply, flags);
+    }
+
+    public IBinder asBinder()
+    {
+        return this;
+    }
+}
+
+
+final class BulkCursorProxy implements IBulkCursor {
+    @UnsupportedAppUsage
+    private IBinder mRemote;
+    private Bundle mExtras;
+
+    public BulkCursorProxy(IBinder remote)
+    {
+        mRemote = remote;
+        mExtras = null;
+    }
+
+    public IBinder asBinder()
+    {
+        return mRemote;
+    }
+
+    public CursorWindow getWindow(int position) throws RemoteException
+    {
+        Parcel data = Parcel.obtain();
+        Parcel reply = Parcel.obtain();
+        try {
+            data.writeInterfaceToken(IBulkCursor.descriptor);
+            data.writeInt(position);
+
+            mRemote.transact(GET_CURSOR_WINDOW_TRANSACTION, data, reply, 0);
+            DatabaseUtils.readExceptionFromParcel(reply);
+
+            CursorWindow window = null;
+            if (reply.readInt() == 1) {
+                window = CursorWindow.newFromParcel(reply);
+            }
+            return window;
+        } finally {
+            data.recycle();
+            reply.recycle();
+        }
+    }
+
+    public void onMove(int position) throws RemoteException {
+        Parcel data = Parcel.obtain();
+        Parcel reply = Parcel.obtain();
+        try {
+            data.writeInterfaceToken(IBulkCursor.descriptor);
+            data.writeInt(position);
+
+            mRemote.transact(ON_MOVE_TRANSACTION, data, reply, 0);
+            DatabaseUtils.readExceptionFromParcel(reply);
+        } finally {
+            data.recycle();
+            reply.recycle();
+        }
+    }
+
+    public void deactivate() throws RemoteException
+    {
+        Parcel data = Parcel.obtain();
+        Parcel reply = Parcel.obtain();
+        try {
+            data.writeInterfaceToken(IBulkCursor.descriptor);
+
+            mRemote.transact(DEACTIVATE_TRANSACTION, data, reply, 0);
+            DatabaseUtils.readExceptionFromParcel(reply);
+        } finally {
+            data.recycle();
+            reply.recycle();
+        }
+    }
+
+    public void close() throws RemoteException
+    {
+        Parcel data = Parcel.obtain();
+        Parcel reply = Parcel.obtain();
+        try {
+            data.writeInterfaceToken(IBulkCursor.descriptor);
+
+            mRemote.transact(CLOSE_TRANSACTION, data, reply, 0);
+            DatabaseUtils.readExceptionFromParcel(reply);
+        } finally {
+            data.recycle();
+            reply.recycle();
+        }
+    }
+    
+    public int requery(IContentObserver observer) throws RemoteException {
+        Parcel data = Parcel.obtain();
+        Parcel reply = Parcel.obtain();
+        try {
+            data.writeInterfaceToken(IBulkCursor.descriptor);
+            data.writeStrongInterface(observer);
+
+            boolean result = mRemote.transact(REQUERY_TRANSACTION, data, reply, 0);
+            DatabaseUtils.readExceptionFromParcel(reply);
+
+            int count;
+            if (!result) {
+                count = -1;
+            } else {
+                count = reply.readInt();
+                mExtras = reply.readBundle();
+            }
+            return count;
+        } finally {
+            data.recycle();
+            reply.recycle();
+        }
+    }
+
+    public Bundle getExtras() throws RemoteException {
+        if (mExtras == null) {
+            Parcel data = Parcel.obtain();
+            Parcel reply = Parcel.obtain();
+            try {
+                data.writeInterfaceToken(IBulkCursor.descriptor);
+
+                mRemote.transact(GET_EXTRAS_TRANSACTION, data, reply, 0);
+                DatabaseUtils.readExceptionFromParcel(reply);
+
+                mExtras = reply.readBundle();
+            } finally {
+                data.recycle();
+                reply.recycle();
+            }
+        }
+        return mExtras;
+    }
+
+    public Bundle respond(Bundle extras) throws RemoteException {
+        Parcel data = Parcel.obtain();
+        Parcel reply = Parcel.obtain();
+        try {
+            data.writeInterfaceToken(IBulkCursor.descriptor);
+            data.writeBundle(extras);
+
+            mRemote.transact(RESPOND_TRANSACTION, data, reply, 0);
+            DatabaseUtils.readExceptionFromParcel(reply);
+
+            Bundle returnExtras = reply.readBundle();
+            return returnExtras;
+        } finally {
+            data.recycle();
+            reply.recycle();
+        }
+    }
+}
+
diff --git a/android/database/BulkCursorToCursorAdaptor.java b/android/database/BulkCursorToCursorAdaptor.java
new file mode 100644
index 0000000..8576715
--- /dev/null
+++ b/android/database/BulkCursorToCursorAdaptor.java
@@ -0,0 +1,190 @@
+/*
+ * 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 android.database;
+
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * Adapts an {@link IBulkCursor} to a {@link Cursor} for use in the local process.
+ *
+ * {@hide}
+ */
+public final class BulkCursorToCursorAdaptor extends AbstractWindowedCursor {
+    private static final String TAG = "BulkCursor";
+
+    private SelfContentObserver mObserverBridge = new SelfContentObserver(this);
+    private IBulkCursor mBulkCursor;
+    private String[] mColumns;
+    private boolean mWantsAllOnMoveCalls;
+    private int mCount;
+
+    /**
+     * Initializes the adaptor.
+     * Must be called before first use.
+     */
+    public void initialize(BulkCursorDescriptor d) {
+        mBulkCursor = d.cursor;
+        mColumns = d.columnNames;
+        mWantsAllOnMoveCalls = d.wantsAllOnMoveCalls;
+        mCount = d.count;
+        if (d.window != null) {
+            setWindow(d.window);
+        }
+    }
+
+    /**
+     * Gets a SelfDataChangeOberserver that can be sent to a remote
+     * process to receive change notifications over IPC.
+     *
+     * @return A SelfContentObserver hooked up to this Cursor
+     */
+    public IContentObserver getObserver() {
+        return mObserverBridge.getContentObserver();
+    }
+
+    private void throwIfCursorIsClosed() {
+        if (mBulkCursor == null) {
+            throw new StaleDataException("Attempted to access a cursor after it has been closed.");
+        }
+    }
+
+    @Override
+    public int getCount() {
+        throwIfCursorIsClosed();
+        return mCount;
+    }
+
+    @Override
+    public boolean onMove(int oldPosition, int newPosition) {
+        throwIfCursorIsClosed();
+
+        try {
+            // Make sure we have the proper window
+            if (mWindow == null
+                    || newPosition < mWindow.getStartPosition()
+                    || newPosition >= mWindow.getStartPosition() + mWindow.getNumRows()) {
+                setWindow(mBulkCursor.getWindow(newPosition));
+            } else if (mWantsAllOnMoveCalls) {
+                mBulkCursor.onMove(newPosition);
+            }
+        } catch (RemoteException ex) {
+            // We tried to get a window and failed
+            Log.e(TAG, "Unable to get window because the remote process is dead");
+            return false;
+        }
+
+        // Couldn't obtain a window, something is wrong
+        if (mWindow == null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public void deactivate() {
+        // This will call onInvalidated(), so make sure to do it before calling release,
+        // which is what actually makes the data set invalid.
+        super.deactivate();
+
+        if (mBulkCursor != null) {
+            try {
+                mBulkCursor.deactivate();
+            } catch (RemoteException ex) {
+                Log.w(TAG, "Remote process exception when deactivating");
+            }
+        }
+    }
+    
+    @Override
+    public void close() {
+        super.close();
+
+        if (mBulkCursor != null) {
+            try {
+                mBulkCursor.close();
+            } catch (RemoteException ex) {
+                Log.w(TAG, "Remote process exception when closing");
+            } finally {
+                mBulkCursor = null;
+            }
+        }
+    }
+
+    @Override
+    public boolean requery() {
+        throwIfCursorIsClosed();
+
+        try {
+            mCount = mBulkCursor.requery(getObserver());
+            if (mCount != -1) {
+                mPos = -1;
+                closeWindow();
+
+                // super.requery() will call onChanged. Do it here instead of relying on the
+                // observer from the far side so that observers can see a correct value for mCount
+                // when responding to onChanged.
+                super.requery();
+                return true;
+            } else {
+                deactivate();
+                return false;
+            }
+        } catch (Exception ex) {
+            Log.e(TAG, "Unable to requery because the remote process exception " + ex.getMessage());
+            deactivate();
+            return false;
+        }
+    }
+
+    @Override
+    public String[] getColumnNames() {
+        throwIfCursorIsClosed();
+
+        return mColumns;
+    }
+
+    @Override
+    public Bundle getExtras() {
+        throwIfCursorIsClosed();
+
+        try {
+            return mBulkCursor.getExtras();
+        } catch (RemoteException e) {
+            // This should never happen because the system kills processes that are using remote
+            // cursors when the provider process is killed.
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public Bundle respond(Bundle extras) {
+        throwIfCursorIsClosed();
+
+        try {
+            return mBulkCursor.respond(extras);
+        } catch (RemoteException e) {
+            // the system kills processes that are using remote cursors when the provider process
+            // is killed, but this can still happen if this is being called from the system process,
+            // so, better to log and return an empty bundle.
+            Log.w(TAG, "respond() threw RemoteException, returning an empty bundle.", e);
+            return Bundle.EMPTY;
+        }
+    }
+}
diff --git a/android/database/CharArrayBuffer.java b/android/database/CharArrayBuffer.java
new file mode 100644
index 0000000..73781b7
--- /dev/null
+++ b/android/database/CharArrayBuffer.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2008 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 android.database;
+
+/**
+ * This is used for {@link Cursor#copyStringToBuffer}
+ */
+public final class CharArrayBuffer {
+    public CharArrayBuffer(int size) {
+        data = new char[size];
+    }
+    
+    public CharArrayBuffer(char[] buf) {
+        data = buf;
+    }
+    
+    public char[] data; // In and out parameter
+    public int sizeCopied; // Out parameter
+}
diff --git a/android/database/ContentObservable.java b/android/database/ContentObservable.java
new file mode 100644
index 0000000..7692bb3
--- /dev/null
+++ b/android/database/ContentObservable.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2007 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 android.database;
+
+import android.net.Uri;
+
+/**
+ * A specialization of {@link Observable} for {@link ContentObserver}
+ * that provides methods for sending notifications to a list of
+ * {@link ContentObserver} objects.
+ */
+public class ContentObservable extends Observable<ContentObserver> {
+    // Even though the generic method defined in Observable would be perfectly
+    // fine on its own, we can't delete this overridden method because it would
+    // potentially break binary compatibility with existing applications.
+    @Override
+    public void registerObserver(ContentObserver observer) {
+        super.registerObserver(observer);
+    }
+
+    /**
+     * Invokes {@link ContentObserver#dispatchChange(boolean)} on each observer.
+     * <p>
+     * If <code>selfChange</code> is true, only delivers the notification
+     * to the observer if it has indicated that it wants to receive self-change
+     * notifications by implementing {@link ContentObserver#deliverSelfNotifications}
+     * to return true.
+     * </p>
+     *
+     * @param selfChange True if this is a self-change notification.
+     *
+     * @deprecated Use {@link #dispatchChange(boolean, Uri)} instead.
+     */
+    @Deprecated
+    public void dispatchChange(boolean selfChange) {
+        dispatchChange(selfChange, null);
+    }
+
+    /**
+     * Invokes {@link ContentObserver#dispatchChange(boolean, Uri)} on each observer.
+     * Includes the changed content Uri when available.
+     * <p>
+     * If <code>selfChange</code> is true, only delivers the notification
+     * to the observer if it has indicated that it wants to receive self-change
+     * notifications by implementing {@link ContentObserver#deliverSelfNotifications}
+     * to return true.
+     * </p>
+     *
+     * @param selfChange True if this is a self-change notification.
+     * @param uri The Uri of the changed content, or null if unknown.
+     */
+    public void dispatchChange(boolean selfChange, Uri uri) {
+        synchronized(mObservers) {
+            for (ContentObserver observer : mObservers) {
+                if (!selfChange || observer.deliverSelfNotifications()) {
+                    observer.dispatchChange(selfChange, uri);
+                }
+            }
+        }
+    }
+
+    /**
+     * Invokes {@link ContentObserver#onChange} on each observer.
+     *
+     * @param selfChange True if this is a self-change notification.
+     *
+     * @deprecated Use {@link #dispatchChange} instead.
+     */
+    @Deprecated
+    public void notifyChange(boolean selfChange) {
+        synchronized(mObservers) {
+            for (ContentObserver observer : mObservers) {
+                observer.onChange(selfChange, null);
+            }
+        }
+    }
+}
diff --git a/android/database/ContentObserver.java b/android/database/ContentObserver.java
new file mode 100644
index 0000000..578d53b
--- /dev/null
+++ b/android/database/ContentObserver.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2007 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 android.database;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ContentResolver.NotifyFlags;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.UserHandle;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+/**
+ * Receives call backs for changes to content.
+ * Must be implemented by objects which are added to a {@link ContentObservable}.
+ */
+public abstract class ContentObserver {
+    /**
+     * Starting in {@link android.os.Build.VERSION_CODES#R}, there is a new
+     * public API overload {@link #onChange(boolean, Uri, int)} that delivers a
+     * {@code int flags} argument.
+     * <p>
+     * Some apps may be relying on a previous hidden API that delivered a
+     * {@code int userId} argument, and this change is used to control delivery
+     * of the new {@code int flags} argument in its place.
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion=android.os.Build.VERSION_CODES.Q)
+    private static final long ADD_CONTENT_OBSERVER_FLAGS = 150939131L;
+
+    private final Object mLock = new Object();
+    private Transport mTransport; // guarded by mLock
+
+    Handler mHandler;
+
+    /**
+     * Creates a content observer.
+     *
+     * @param handler The handler to run {@link #onChange} on, or null if none.
+     */
+    public ContentObserver(Handler handler) {
+        mHandler = handler;
+    }
+
+    /**
+     * Gets access to the binder transport object. Not for public consumption.
+     *
+     * {@hide}
+     */
+    public IContentObserver getContentObserver() {
+        synchronized (mLock) {
+            if (mTransport == null) {
+                mTransport = new Transport(this);
+            }
+            return mTransport;
+        }
+    }
+
+    /**
+     * Gets access to the binder transport object, and unlinks the transport object
+     * from the ContentObserver. Not for public consumption.
+     *
+     * {@hide}
+     */
+    @UnsupportedAppUsage
+    public IContentObserver releaseContentObserver() {
+        synchronized (mLock) {
+            final Transport oldTransport = mTransport;
+            if (oldTransport != null) {
+                oldTransport.releaseContentObserver();
+                mTransport = null;
+            }
+            return oldTransport;
+        }
+    }
+
+    /**
+     * Returns true if this observer is interested receiving self-change notifications.
+     *
+     * Subclasses should override this method to indicate whether the observer
+     * is interested in receiving notifications for changes that it made to the
+     * content itself.
+     *
+     * @return True if self-change notifications should be delivered to the observer.
+     */
+    public boolean deliverSelfNotifications() {
+        return false;
+    }
+
+    /**
+     * This method is called when a content change occurs.
+     * <p>
+     * Subclasses should override this method to handle content changes.
+     * </p>
+     *
+     * @param selfChange True if this is a self-change notification.
+     */
+    public void onChange(boolean selfChange) {
+        // Do nothing.  Subclass should override.
+    }
+
+    /**
+     * This method is called when a content change occurs.
+     * Includes the changed content Uri when available.
+     * <p>
+     * Subclasses should override this method to handle content changes. To
+     * ensure correct operation on older versions of the framework that did not
+     * provide richer arguments, applications should implement all overloads.
+     * <p>
+     * Example implementation:
+     * <pre><code>
+     * // Implement the onChange(boolean) method to delegate the change notification to
+     * // the onChange(boolean, Uri) method to ensure correct operation on older versions
+     * // of the framework that did not have the onChange(boolean, Uri) method.
+     * {@literal @Override}
+     * public void onChange(boolean selfChange) {
+     *     onChange(selfChange, null);
+     * }
+     *
+     * // Implement the onChange(boolean, Uri) method to take advantage of the new Uri argument.
+     * {@literal @Override}
+     * public void onChange(boolean selfChange, Uri uri) {
+     *     // Handle change.
+     * }
+     * </code></pre>
+     * </p>
+     *
+     * @param selfChange True if this is a self-change notification.
+     * @param uri The Uri of the changed content.
+     */
+    public void onChange(boolean selfChange, @Nullable Uri uri) {
+        onChange(selfChange);
+    }
+
+    /**
+     * This method is called when a content change occurs. Includes the changed
+     * content Uri when available.
+     * <p>
+     * Subclasses should override this method to handle content changes. To
+     * ensure correct operation on older versions of the framework that did not
+     * provide richer arguments, applications should implement all overloads.
+     *
+     * @param selfChange True if this is a self-change notification.
+     * @param uri The Uri of the changed content.
+     * @param flags Flags indicating details about this change.
+     */
+    public void onChange(boolean selfChange, @Nullable Uri uri, @NotifyFlags int flags) {
+        onChange(selfChange, uri);
+    }
+
+    /**
+     * This method is called when a content change occurs. Includes the changed
+     * content Uris when available.
+     * <p>
+     * Subclasses should override this method to handle content changes. To
+     * ensure correct operation on older versions of the framework that did not
+     * provide richer arguments, applications should implement all overloads.
+     *
+     * @param selfChange True if this is a self-change notification.
+     * @param uris The Uris of the changed content.
+     * @param flags Flags indicating details about this change.
+     */
+    public void onChange(boolean selfChange, @NonNull Collection<Uri> uris,
+            @NotifyFlags int flags) {
+        for (Uri uri : uris) {
+            onChange(selfChange, uri, flags);
+        }
+    }
+
+    /** @hide */
+    public void onChange(boolean selfChange, @NonNull Collection<Uri> uris,
+            @NotifyFlags int flags, @UserIdInt int userId) {
+        // There are dozens of people relying on the hidden API inside the
+        // system UID, so hard-code the old behavior for all of them; for
+        // everyone else we gate based on a specific change
+        if (!CompatChanges.isChangeEnabled(ADD_CONTENT_OBSERVER_FLAGS)
+                || android.os.Process.myUid() == android.os.Process.SYSTEM_UID) {
+            // Deliver userId through argument to preserve hidden API behavior
+            onChange(selfChange, uris, userId);
+        } else {
+            onChange(selfChange, uris, flags);
+        }
+    }
+
+    /**
+     * Dispatches a change notification to the observer.
+     * <p>
+     * If a {@link Handler} was supplied to the {@link ContentObserver}
+     * constructor, then a call to the {@link #onChange} method is posted to the
+     * handler's message queue. Otherwise, the {@link #onChange} method is
+     * invoked immediately on this thread.
+     *
+     * @deprecated Callers should migrate towards using a richer overload that
+     *             provides more details about the change, such as
+     *             {@link #dispatchChange(boolean, Collection, int)}.
+     */
+    @Deprecated
+    public final void dispatchChange(boolean selfChange) {
+        dispatchChange(selfChange, null);
+    }
+
+    /**
+     * Dispatches a change notification to the observer. Includes the changed
+     * content Uri when available.
+     * <p>
+     * If a {@link Handler} was supplied to the {@link ContentObserver}
+     * constructor, then a call to the {@link #onChange} method is posted to the
+     * handler's message queue. Otherwise, the {@link #onChange} method is
+     * invoked immediately on this thread.
+     *
+     * @param selfChange True if this is a self-change notification.
+     * @param uri The Uri of the changed content.
+     */
+    public final void dispatchChange(boolean selfChange, @Nullable Uri uri) {
+        dispatchChange(selfChange, uri, 0);
+    }
+
+    /**
+     * Dispatches a change notification to the observer. Includes the changed
+     * content Uri when available.
+     * <p>
+     * If a {@link Handler} was supplied to the {@link ContentObserver}
+     * constructor, then a call to the {@link #onChange} method is posted to the
+     * handler's message queue. Otherwise, the {@link #onChange} method is
+     * invoked immediately on this thread.
+     *
+     * @param selfChange True if this is a self-change notification.
+     * @param uri The Uri of the changed content.
+     * @param flags Flags indicating details about this change.
+     */
+    public final void dispatchChange(boolean selfChange, @Nullable Uri uri,
+            @NotifyFlags int flags) {
+        dispatchChange(selfChange, Arrays.asList(uri), flags);
+    }
+
+    /**
+     * Dispatches a change notification to the observer. Includes the changed
+     * content Uris when available.
+     * <p>
+     * If a {@link Handler} was supplied to the {@link ContentObserver}
+     * constructor, then a call to the {@link #onChange} method is posted to the
+     * handler's message queue. Otherwise, the {@link #onChange} method is
+     * invoked immediately on this thread.
+     *
+     * @param selfChange True if this is a self-change notification.
+     * @param uris The Uri of the changed content.
+     * @param flags Flags indicating details about this change.
+     */
+    public final void dispatchChange(boolean selfChange, @NonNull Collection<Uri> uris,
+            @NotifyFlags int flags) {
+        dispatchChange(selfChange, uris, flags, UserHandle.getCallingUserId());
+    }
+
+    /** @hide */
+    public final void dispatchChange(boolean selfChange, @NonNull Collection<Uri> uris,
+            @NotifyFlags int flags, @UserIdInt int userId) {
+        if (mHandler == null) {
+            onChange(selfChange, uris, flags, userId);
+        } else {
+            mHandler.post(() -> {
+                onChange(selfChange, uris, flags, userId);
+            });
+        }
+    }
+
+    private static final class Transport extends IContentObserver.Stub {
+        private ContentObserver mContentObserver;
+
+        public Transport(ContentObserver contentObserver) {
+            mContentObserver = contentObserver;
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri, int userId) {
+            // This is kept intact purely for apps using hidden APIs, to
+            // redirect to the updated implementation
+            onChangeEtc(selfChange, new Uri[] { uri }, 0, userId);
+        }
+
+        @Override
+        public void onChangeEtc(boolean selfChange, Uri[] uris, int flags, int userId) {
+            ContentObserver contentObserver = mContentObserver;
+            if (contentObserver != null) {
+                contentObserver.dispatchChange(selfChange, Arrays.asList(uris), flags, userId);
+            }
+        }
+
+        public void releaseContentObserver() {
+            mContentObserver = null;
+        }
+    }
+}
diff --git a/android/database/CrossProcessCursor.java b/android/database/CrossProcessCursor.java
new file mode 100644
index 0000000..26379cc
--- /dev/null
+++ b/android/database/CrossProcessCursor.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2008 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 android.database;
+
+/**
+ * A cross process cursor is an extension of a {@link Cursor} that also supports
+ * usage from remote processes.
+ * <p>
+ * The contents of a cross process cursor are marshalled to the remote process by
+ * filling {@link CursorWindow} objects using {@link #fillWindow}.  As an optimization,
+ * the cursor can provide a pre-filled window to use via {@link #getWindow} thereby
+ * obviating the need to copy the data to yet another cursor window.
+ */
+public interface CrossProcessCursor extends Cursor {
+    /**
+     * Returns a pre-filled window that contains the data within this cursor.
+     * <p>
+     * In particular, the window contains the row indicated by {@link Cursor#getPosition}.
+     * The window's contents are automatically scrolled whenever the current
+     * row moved outside the range covered by the window.
+     * </p>
+     *
+     * @return The pre-filled window, or null if none.
+     */
+    CursorWindow getWindow();
+
+    /**
+     * Copies cursor data into the window.
+     * <p>
+     * Clears the window and fills it with data beginning at the requested
+     * row position until all of the data in the cursor is exhausted
+     * or the window runs out of space.
+     * </p><p>
+     * The filled window uses the same row indices as the original cursor.
+     * For example, if you fill a window starting from row 5 from the cursor,
+     * you can query the contents of row 5 from the window just by asking it
+     * for row 5 because there is a direct correspondence between the row indices
+     * used by the cursor and the window.
+     * </p><p>
+     * The current position of the cursor, as returned by {@link #getPosition},
+     * is not changed by this method.
+     * </p>
+     *
+     * @param position The zero-based index of the first row to copy into the window.
+     * @param window The window to fill.
+     */
+    void fillWindow(int position, CursorWindow window);
+
+    /**
+     * This function is called every time the cursor is successfully scrolled
+     * to a new position, giving the subclass a chance to update any state it
+     * may have.  If it returns false the move function will also do so and the
+     * cursor will scroll to the beforeFirst position.
+     * <p>
+     * This function should be called by methods such as {@link #moveToPosition(int)},
+     * so it will typically not be called from outside of the cursor class itself.
+     * </p>
+     *
+     * @param oldPosition The position that we're moving from.
+     * @param newPosition The position that we're moving to.
+     * @return True if the move is successful, false otherwise.
+     */
+    boolean onMove(int oldPosition, int newPosition); 
+}
diff --git a/android/database/CrossProcessCursorWrapper.java b/android/database/CrossProcessCursorWrapper.java
new file mode 100644
index 0000000..1b77cb9
--- /dev/null
+++ b/android/database/CrossProcessCursorWrapper.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2011 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 android.database;
+
+import android.database.CrossProcessCursor;
+import android.database.Cursor;
+import android.database.CursorWindow;
+import android.database.CursorWrapper;
+
+/**
+ * Cursor wrapper that implements {@link CrossProcessCursor}.
+ * <p>
+ * If the wrapped cursor implements {@link CrossProcessCursor}, then the wrapper
+ * delegates {@link #fillWindow}, {@link #getWindow()} and {@link #onMove} to it.
+ * Otherwise, the wrapper provides default implementations of these methods that
+ * traverse the contents of the cursor similar to {@link AbstractCursor#fillWindow}.
+ * </p><p>
+ * This wrapper can be used to adapt an ordinary {@link Cursor} into a
+ * {@link CrossProcessCursor}.
+ * </p>
+ */
+public class CrossProcessCursorWrapper extends CursorWrapper implements CrossProcessCursor {
+    /**
+     * Creates a cross process cursor wrapper.
+     * @param cursor The underlying cursor to wrap.
+     */
+    public CrossProcessCursorWrapper(Cursor cursor) {
+        super(cursor);
+    }
+
+    @Override
+    public void fillWindow(int position, CursorWindow window) {
+        if (mCursor instanceof CrossProcessCursor) {
+            final CrossProcessCursor crossProcessCursor = (CrossProcessCursor)mCursor;
+            crossProcessCursor.fillWindow(position, window);
+            return;
+        }
+
+        DatabaseUtils.cursorFillWindow(mCursor, position, window);
+    }
+
+    @Override
+    public CursorWindow getWindow() {
+        if (mCursor instanceof CrossProcessCursor) {
+            final CrossProcessCursor crossProcessCursor = (CrossProcessCursor)mCursor;
+            return crossProcessCursor.getWindow();
+        }
+
+        return null;
+    }
+
+    @Override
+    public boolean onMove(int oldPosition, int newPosition) {
+        if (mCursor instanceof CrossProcessCursor) {
+            final CrossProcessCursor crossProcessCursor = (CrossProcessCursor)mCursor;
+            return crossProcessCursor.onMove(oldPosition, newPosition);
+        }
+
+        return true;
+    }
+}
diff --git a/android/database/Cursor.java b/android/database/Cursor.java
new file mode 100644
index 0000000..2afb755
--- /dev/null
+++ b/android/database/Cursor.java
@@ -0,0 +1,518 @@
+/*
+ * 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 android.database;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.os.Bundle;
+
+import java.io.Closeable;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * This interface provides random read-write access to the result set returned
+ * by a database query.
+ * <p>
+ * Cursor implementations are not required to be synchronized so code using a Cursor from multiple
+ * threads should perform its own synchronization when using the Cursor.
+ * </p><p>
+ * Implementations should subclass {@link AbstractCursor}.
+ * </p>
+ */
+public interface Cursor extends Closeable {
+    /*
+     * Values returned by {@link #getType(int)}.
+     * These should be consistent with the corresponding types defined in CursorWindow.h
+     */
+    /** Value returned by {@link #getType(int)} if the specified column is null */
+    static final int FIELD_TYPE_NULL = 0;
+
+    /** Value returned by {@link #getType(int)} if the specified  column type is integer */
+    static final int FIELD_TYPE_INTEGER = 1;
+
+    /** Value returned by {@link #getType(int)} if the specified column type is float */
+    static final int FIELD_TYPE_FLOAT = 2;
+
+    /** Value returned by {@link #getType(int)} if the specified column type is string */
+    static final int FIELD_TYPE_STRING = 3;
+
+    /** Value returned by {@link #getType(int)} if the specified column type is blob */
+    static final int FIELD_TYPE_BLOB = 4;
+
+    /**
+     * Returns the numbers of rows in the cursor.
+     *
+     * @return the number of rows in the cursor.
+     */
+    int getCount();
+
+    /**
+     * Returns the current position of the cursor in the row set.
+     * The value is zero-based. When the row set is first returned the cursor
+     * will be at positon -1, which is before the first row. After the
+     * last row is returned another call to next() will leave the cursor past
+     * the last entry, at a position of count().
+     *
+     * @return the current cursor position.
+     */
+    int getPosition();
+
+    /**
+     * Move the cursor by a relative amount, forward or backward, from the
+     * current position. Positive offsets move forwards, negative offsets move
+     * backwards. If the final position is outside of the bounds of the result
+     * set then the resultant position will be pinned to -1 or count() depending
+     * on whether the value is off the front or end of the set, respectively.
+     *
+     * <p>This method will return true if the requested destination was
+     * reachable, otherwise, it returns false. For example, if the cursor is at
+     * currently on the second entry in the result set and move(-5) is called,
+     * the position will be pinned at -1, and false will be returned.
+     *
+     * @param offset the offset to be applied from the current position.
+     * @return whether the requested move fully succeeded.
+     */
+    boolean move(int offset);
+
+    /**
+     * Move the cursor to an absolute position. The valid
+     * range of values is -1 &lt;= position &lt;= count.
+     *
+     * <p>This method will return true if the request destination was reachable, 
+     * otherwise, it returns false.
+     *
+     * @param position the zero-based position to move to.
+     * @return whether the requested move fully succeeded.
+     */
+    boolean moveToPosition(int position);
+
+    /**
+     * Move the cursor to the first row.
+     *
+     * <p>This method will return false if the cursor is empty.
+     *
+     * @return whether the move succeeded.
+     */
+    boolean moveToFirst();
+
+    /**
+     * Move the cursor to the last row.
+     *
+     * <p>This method will return false if the cursor is empty.
+     *
+     * @return whether the move succeeded.
+     */
+    boolean moveToLast();
+
+    /**
+     * Move the cursor to the next row.
+     *
+     * <p>This method will return false if the cursor is already past the
+     * last entry in the result set.
+     *
+     * @return whether the move succeeded.
+     */
+    boolean moveToNext();
+
+    /**
+     * Move the cursor to the previous row.
+     *
+     * <p>This method will return false if the cursor is already before the
+     * first entry in the result set.
+     *
+     * @return whether the move succeeded.
+     */
+    boolean moveToPrevious();
+
+    /**
+     * Returns whether the cursor is pointing to the first row.
+     *
+     * @return whether the cursor is pointing at the first entry.
+     */
+    boolean isFirst();
+
+    /**
+     * Returns whether the cursor is pointing to the last row.
+     *
+     * @return whether the cursor is pointing at the last entry.
+     */
+    boolean isLast();
+
+    /**
+     * Returns whether the cursor is pointing to the position before the first
+     * row.
+     *
+     * @return whether the cursor is before the first result.
+     */
+    boolean isBeforeFirst();
+
+    /**
+     * Returns whether the cursor is pointing to the position after the last
+     * row.
+     *
+     * @return whether the cursor is after the last result.
+     */
+    boolean isAfterLast();
+
+    /**
+     * Returns the zero-based index for the given column name, or -1 if the column doesn't exist.
+     * If you expect the column to exist use {@link #getColumnIndexOrThrow(String)} instead, which
+     * will make the error more clear.
+     *
+     * @param columnName the name of the target column.
+     * @return the zero-based column index for the given column name, or -1 if
+     * the column name does not exist.
+     * @see #getColumnIndexOrThrow(String)
+     */
+    int getColumnIndex(String columnName);
+
+    /**
+     * Returns the zero-based index for the given column name, or throws
+     * {@link IllegalArgumentException} if the column doesn't exist. If you're not sure if
+     * a column will exist or not use {@link #getColumnIndex(String)} and check for -1, which
+     * is more efficient than catching the exceptions.
+     *
+     * @param columnName the name of the target column.
+     * @return the zero-based column index for the given column name
+     * @see #getColumnIndex(String)
+     * @throws IllegalArgumentException if the column does not exist
+     */
+    int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException;
+
+    /**
+     * Returns the column name at the given zero-based column index.
+     *
+     * @param columnIndex the zero-based index of the target column.
+     * @return the column name for the given column index.
+     */
+    String getColumnName(int columnIndex);
+
+    /**
+     * Returns a string array holding the names of all of the columns in the
+     * result set in the order in which they were listed in the result.
+     *
+     * @return the names of the columns returned in this query.
+     */
+    String[] getColumnNames();
+
+    /**
+     * Return total number of columns
+     * @return number of columns 
+     */
+    int getColumnCount();
+    
+    /**
+     * Returns the value of the requested column as a byte array.
+     *
+     * <p>The result and whether this method throws an exception when the
+     * column value is null or the column type is not a blob type is
+     * implementation-defined.
+     *
+     * @param columnIndex the zero-based index of the target column.
+     * @return the value of that column as a byte array.
+     */
+    byte[] getBlob(int columnIndex);
+
+    /**
+     * Returns the value of the requested column as a String.
+     *
+     * <p>The result and whether this method throws an exception when the
+     * column value is null or the column type is not a string type is
+     * implementation-defined.
+     *
+     * @param columnIndex the zero-based index of the target column.
+     * @return the value of that column as a String.
+     */
+    String getString(int columnIndex);
+    
+    /**
+     * Retrieves the requested column text and stores it in the buffer provided.
+     * If the buffer size is not sufficient, a new char buffer will be allocated 
+     * and assigned to CharArrayBuffer.data
+     * @param columnIndex the zero-based index of the target column.
+     *        if the target column is null, return buffer
+     * @param buffer the buffer to copy the text into. 
+     */
+    void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer);
+    
+    /**
+     * Returns the value of the requested column as a short.
+     *
+     * <p>The result and whether this method throws an exception when the
+     * column value is null, the column type is not an integral type, or the
+     * integer value is outside the range [<code>Short.MIN_VALUE</code>,
+     * <code>Short.MAX_VALUE</code>] is implementation-defined.
+     *
+     * @param columnIndex the zero-based index of the target column.
+     * @return the value of that column as a short.
+     */
+    short getShort(int columnIndex);
+
+    /**
+     * Returns the value of the requested column as an int.
+     *
+     * <p>The result and whether this method throws an exception when the
+     * column value is null, the column type is not an integral type, or the
+     * integer value is outside the range [<code>Integer.MIN_VALUE</code>,
+     * <code>Integer.MAX_VALUE</code>] is implementation-defined.
+     *
+     * @param columnIndex the zero-based index of the target column.
+     * @return the value of that column as an int.
+     */
+    int getInt(int columnIndex);
+
+    /**
+     * Returns the value of the requested column as a long.
+     *
+     * <p>The result and whether this method throws an exception when the
+     * column value is null, the column type is not an integral type, or the
+     * integer value is outside the range [<code>Long.MIN_VALUE</code>,
+     * <code>Long.MAX_VALUE</code>] is implementation-defined.
+     *
+     * @param columnIndex the zero-based index of the target column.
+     * @return the value of that column as a long.
+     */
+    long getLong(int columnIndex);
+
+    /**
+     * Returns the value of the requested column as a float.
+     *
+     * <p>The result and whether this method throws an exception when the
+     * column value is null, the column type is not a floating-point type, or the
+     * floating-point value is not representable as a <code>float</code> value is
+     * implementation-defined.
+     *
+     * @param columnIndex the zero-based index of the target column.
+     * @return the value of that column as a float.
+     */
+    float getFloat(int columnIndex);
+
+    /**
+     * Returns the value of the requested column as a double.
+     *
+     * <p>The result and whether this method throws an exception when the
+     * column value is null, the column type is not a floating-point type, or the
+     * floating-point value is not representable as a <code>double</code> value is
+     * implementation-defined.
+     *
+     * @param columnIndex the zero-based index of the target column.
+     * @return the value of that column as a double.
+     */
+    double getDouble(int columnIndex);
+
+    /**
+     * Returns data type of the given column's value.
+     * The preferred type of the column is returned but the data may be converted to other types
+     * as documented in the get-type methods such as {@link #getInt(int)}, {@link #getFloat(int)}
+     * etc.
+     *<p>
+     * Returned column types are
+     * <ul>
+     *   <li>{@link #FIELD_TYPE_NULL}</li>
+     *   <li>{@link #FIELD_TYPE_INTEGER}</li>
+     *   <li>{@link #FIELD_TYPE_FLOAT}</li>
+     *   <li>{@link #FIELD_TYPE_STRING}</li>
+     *   <li>{@link #FIELD_TYPE_BLOB}</li>
+     *</ul>
+     *</p>
+     *
+     * @param columnIndex the zero-based index of the target column.
+     * @return column value type
+     */
+    int getType(int columnIndex);
+
+    /**
+     * Returns <code>true</code> if the value in the indicated column is null.
+     *
+     * @param columnIndex the zero-based index of the target column.
+     * @return whether the column value is null.
+     */
+    boolean isNull(int columnIndex);
+
+    /**
+     * Deactivates the Cursor, making all calls on it fail until {@link #requery} is called.
+     * Inactive Cursors use fewer resources than active Cursors.
+     * Calling {@link #requery} will make the cursor active again.
+     * @deprecated Since {@link #requery()} is deprecated, so too is this.
+     */
+    @Deprecated
+    void deactivate();
+
+    /**
+     * Performs the query that created the cursor again, refreshing its 
+     * contents. This may be done at any time, including after a call to {@link
+     * #deactivate}.
+     *
+     * Since this method could execute a query on the database and potentially take
+     * a while, it could cause ANR if it is called on Main (UI) thread.
+     * A warning is printed if this method is being executed on Main thread.
+     *
+     * @return true if the requery succeeded, false if not, in which case the
+     *         cursor becomes invalid.
+     * @deprecated Don't use this. Just request a new cursor, so you can do this
+     * asynchronously and update your list view once the new cursor comes back.
+     */
+    @Deprecated
+    boolean requery();
+
+    /**
+     * Closes the Cursor, releasing all of its resources and making it completely invalid.
+     * Unlike {@link #deactivate()} a call to {@link #requery()} will not make the Cursor valid
+     * again.
+     */
+    void close();
+
+    /**
+     * return true if the cursor is closed
+     * @return true if the cursor is closed.
+     */
+    boolean isClosed();
+    
+    /**
+     * Register an observer that is called when changes happen to the content backing this cursor.
+     * Typically the data set won't change until {@link #requery()} is called.
+     *
+     * @param observer the object that gets notified when the content backing the cursor changes.
+     * @see #unregisterContentObserver(ContentObserver)
+     */
+    void registerContentObserver(ContentObserver observer);
+
+    /**
+     * Unregister an observer that has previously been registered with this
+     * cursor via {@link #registerContentObserver}.
+     *
+     * @param observer the object to unregister.
+     * @see #registerContentObserver(ContentObserver)
+     */
+    void unregisterContentObserver(ContentObserver observer);
+    
+    /**
+     * Register an observer that is called when changes happen to the contents
+     * of the this cursors data set, for example, when the data set is changed via
+     * {@link #requery()}, {@link #deactivate()}, or {@link #close()}.
+     *
+     * @param observer the object that gets notified when the cursors data set changes.
+     * @see #unregisterDataSetObserver(DataSetObserver)
+     */
+    void registerDataSetObserver(DataSetObserver observer);
+
+    /**
+     * Unregister an observer that has previously been registered with this
+     * cursor via {@link #registerContentObserver}.
+     *
+     * @param observer the object to unregister.
+     * @see #registerDataSetObserver(DataSetObserver)
+     */
+    void unregisterDataSetObserver(DataSetObserver observer);
+
+    /**
+     * Register to watch a content URI for changes. This can be the URI of a specific data row (for 
+     * example, "content://my_provider_type/23"), or a a generic URI for a content type.
+     *
+     * <p>Calling this overrides any previous call to
+     * {@link #setNotificationUris(ContentResolver, List)}.
+     *
+     * @param cr The content resolver from the caller's context. The listener attached to 
+     * this resolver will be notified.
+     * @param uri The content URI to watch.
+     */
+    void setNotificationUri(ContentResolver cr, Uri uri);
+
+    /**
+     * Similar to {@link #setNotificationUri(ContentResolver, Uri)}, except this version allows
+     * to watch multiple content URIs for changes.
+     *
+     * <p>If this is not implemented, this is equivalent to calling
+     * {@link #setNotificationUri(ContentResolver, Uri)} with the first URI in {@code uris}.
+     *
+     * <p>Calling this overrides any previous call to
+     * {@link #setNotificationUri(ContentResolver, Uri)}.
+     *
+     * @param cr The content resolver from the caller's context. The listener attached to
+     * this resolver will be notified.
+     * @param uris The content URIs to watch.
+     */
+    default void setNotificationUris(@NonNull ContentResolver cr, @NonNull List<Uri> uris) {
+        setNotificationUri(cr, uris.get(0));
+    }
+
+    /**
+     * Return the URI at which notifications of changes in this Cursor's data
+     * will be delivered, as previously set by {@link #setNotificationUri}.
+     * @return Returns a URI that can be used with
+     * {@link ContentResolver#registerContentObserver(android.net.Uri, boolean, ContentObserver)
+     * ContentResolver.registerContentObserver} to find out about changes to this Cursor's
+     * data.  May be null if no notification URI has been set.
+     */
+    Uri getNotificationUri();
+
+    /**
+     * Return the URIs at which notifications of changes in this Cursor's data
+     * will be delivered, as previously set by {@link #setNotificationUris}.
+     *
+     * <p>If this is not implemented, this is equivalent to calling {@link #getNotificationUri()}.
+     *
+     * @return Returns URIs that can be used with
+     * {@link ContentResolver#registerContentObserver(android.net.Uri, boolean, ContentObserver)
+     * ContentResolver.registerContentObserver} to find out about changes to this Cursor's
+     * data. May be null if no notification URI has been set.
+     */
+    default @Nullable List<Uri> getNotificationUris() {
+        final Uri notifyUri = getNotificationUri();
+        return notifyUri == null ? null : Arrays.asList(notifyUri);
+    }
+
+    /**
+     * onMove() will only be called across processes if this method returns true.
+     * @return whether all cursor movement should result in a call to onMove().
+     */
+    boolean getWantsAllOnMoveCalls();
+
+    /**
+     * Sets a {@link Bundle} that will be returned by {@link #getExtras()}.
+     *
+     * @param extras {@link Bundle} to set, or null to set an empty bundle.
+     */
+    void setExtras(Bundle extras);
+
+    /**
+     * Returns a bundle of extra values. This is an optional way for cursors to provide out-of-band
+     * metadata to their users. One use of this is for reporting on the progress of network requests
+     * that are required to fetch data for the cursor.
+     *
+     * <p>These values may only change when requery is called.
+     * @return cursor-defined values, or {@link android.os.Bundle#EMPTY Bundle.EMPTY} if there
+     *         are no values. Never <code>null</code>.
+     */
+    Bundle getExtras();
+
+    /**
+     * This is an out-of-band way for the the user of a cursor to communicate with the cursor. The
+     * structure of each bundle is entirely defined by the cursor.
+     *
+     * <p>One use of this is to tell a cursor that it should retry its network request after it
+     * reported an error.
+     * @param extras extra values, or {@link android.os.Bundle#EMPTY Bundle.EMPTY}.
+     *         Never <code>null</code>.
+     * @return extra values, or {@link android.os.Bundle#EMPTY Bundle.EMPTY}.
+     *         Never <code>null</code>.
+     */
+    Bundle respond(Bundle extras);
+}
diff --git a/android/database/CursorIndexOutOfBoundsException.java b/android/database/CursorIndexOutOfBoundsException.java
new file mode 100644
index 0000000..1f77d00
--- /dev/null
+++ b/android/database/CursorIndexOutOfBoundsException.java
@@ -0,0 +1,31 @@
+/*
+ * 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 android.database;
+
+/**
+ * An exception indicating that a cursor is out of bounds.
+ */
+public class CursorIndexOutOfBoundsException extends IndexOutOfBoundsException {
+
+    public CursorIndexOutOfBoundsException(int index, int size) {
+        super("Index " + index + " requested, with a size of " + size);
+    }
+
+    public CursorIndexOutOfBoundsException(String message) {
+        super(message);
+    }
+}
diff --git a/android/database/CursorJoiner.java b/android/database/CursorJoiner.java
new file mode 100644
index 0000000..a95263b
--- /dev/null
+++ b/android/database/CursorJoiner.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2008 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 android.database;
+
+import java.util.Iterator;
+
+/**
+ * Does a join on two cursors using the specified columns. The cursors must already
+ * be sorted on each of the specified columns in ascending order. This joiner only
+ * supports the case where the tuple of key column values is unique.
+ * <p>
+ * Typical usage:
+ *
+ * <pre>
+ * CursorJoiner joiner = new CursorJoiner(cursorA, keyColumnsofA, cursorB, keyColumnsofB);
+ * for (CursorJoiner.Result joinerResult : joiner) {
+ *     switch (joinerResult) {
+ *         case LEFT:
+ *             // handle case where a row in cursorA is unique
+ *             break;
+ *         case RIGHT:
+ *             // handle case where a row in cursorB is unique
+ *             break;
+ *         case BOTH:
+ *             // handle case where a row with the same key is in both cursors
+ *             break;
+ *     }
+ * }
+ * </pre>
+ */
+public final class CursorJoiner
+        implements Iterator<CursorJoiner.Result>, Iterable<CursorJoiner.Result> {
+    private Cursor mCursorLeft;
+    private Cursor mCursorRight;
+    private boolean mCompareResultIsValid;
+    private Result mCompareResult;
+    private int[] mColumnsLeft;
+    private int[] mColumnsRight;
+    private String[] mValues;
+
+    /**
+     * The result of a call to next().
+     */
+    public enum Result {
+        /** The row currently pointed to by the left cursor is unique */
+        RIGHT,
+        /** The row currently pointed to by the right cursor is unique */
+        LEFT,
+        /** The rows pointed to by both cursors are the same */
+        BOTH
+    }
+
+    /**
+     * Initializes the CursorJoiner and resets the cursors to the first row. The left and right
+     * column name arrays must have the same number of columns.
+     * @param cursorLeft The left cursor to compare
+     * @param columnNamesLeft The column names to compare from the left cursor
+     * @param cursorRight The right cursor to compare
+     * @param columnNamesRight The column names to compare from the right cursor
+     */
+    public CursorJoiner(
+            Cursor cursorLeft, String[] columnNamesLeft,
+            Cursor cursorRight, String[] columnNamesRight) {
+        if (columnNamesLeft.length != columnNamesRight.length) {
+            throw new IllegalArgumentException(
+                    "you must have the same number of columns on the left and right, "
+                            + columnNamesLeft.length + " != " + columnNamesRight.length);
+        }
+
+        mCursorLeft = cursorLeft;
+        mCursorRight = cursorRight;
+
+        mCursorLeft.moveToFirst();
+        mCursorRight.moveToFirst();
+
+        mCompareResultIsValid = false;
+
+        mColumnsLeft = buildColumnIndiciesArray(cursorLeft, columnNamesLeft);
+        mColumnsRight = buildColumnIndiciesArray(cursorRight, columnNamesRight);
+
+        mValues = new String[mColumnsLeft.length * 2];
+    }
+
+    public Iterator<Result> iterator() {
+        return this;
+    }
+
+    /**
+     * Lookup the indicies of the each column name and return them in an array.
+     * @param cursor the cursor that contains the columns
+     * @param columnNames the array of names to lookup
+     * @return an array of column indices
+     */
+    private int[] buildColumnIndiciesArray(Cursor cursor, String[] columnNames) {
+        int[] columns = new int[columnNames.length];
+        for (int i = 0; i < columnNames.length; i++) {
+            columns[i] = cursor.getColumnIndexOrThrow(columnNames[i]);
+        }
+        return columns;
+    }
+
+    /**
+     * Returns whether or not there are more rows to compare using next().
+     * @return true if there are more rows to compare
+     */
+    public boolean hasNext() {
+        if (mCompareResultIsValid) {
+            switch (mCompareResult) {
+                case BOTH:
+                    return !mCursorLeft.isLast() || !mCursorRight.isLast();
+
+                case LEFT:
+                    return !mCursorLeft.isLast() || !mCursorRight.isAfterLast();
+
+                case RIGHT:
+                    return !mCursorLeft.isAfterLast() || !mCursorRight.isLast();
+
+                default:
+                    throw new IllegalStateException("bad value for mCompareResult, "
+                            + mCompareResult);
+            }
+        } else {
+            return !mCursorLeft.isAfterLast() || !mCursorRight.isAfterLast();
+        }
+    }
+
+    /**
+     * Returns the comparison result of the next row from each cursor. If one cursor
+     * has no more rows but the other does then subsequent calls to this will indicate that
+     * the remaining rows are unique.
+     * <p>
+     * The caller must check that hasNext() returns true before calling this.
+     * <p>
+     * Once next() has been called the cursors specified in the result of the call to
+     * next() are guaranteed to point to the row that was indicated. Reading values
+     * from the cursor that was not indicated in the call to next() will result in
+     * undefined behavior.
+     * @return LEFT, if the row pointed to by the left cursor is unique, RIGHT
+     *   if the row pointed to by the right cursor is unique, BOTH if the rows in both
+     *   cursors are the same.
+     */
+    public Result next() {
+        if (!hasNext()) {
+            throw new IllegalStateException("you must only call next() when hasNext() is true");
+        }
+        incrementCursors();
+        assert hasNext();
+
+        boolean hasLeft = !mCursorLeft.isAfterLast();
+        boolean hasRight = !mCursorRight.isAfterLast();
+
+        if (hasLeft && hasRight) {
+            populateValues(mValues, mCursorLeft, mColumnsLeft, 0 /* start filling at index 0 */);
+            populateValues(mValues, mCursorRight, mColumnsRight, 1 /* start filling at index 1 */);
+            switch (compareStrings(mValues)) {
+                case -1:
+                    mCompareResult = Result.LEFT;
+                    break;
+                case 0:
+                    mCompareResult = Result.BOTH;
+                    break;
+                case 1:
+                    mCompareResult = Result.RIGHT;
+                    break;
+            }
+        } else if (hasLeft) {
+            mCompareResult = Result.LEFT;
+        } else  {
+            assert hasRight;
+            mCompareResult = Result.RIGHT;
+        }
+        mCompareResultIsValid = true;
+        return mCompareResult;
+    }
+
+    public void remove() {
+        throw new UnsupportedOperationException("not implemented");
+    }
+
+    /**
+     * Reads the strings from the cursor that are specifed in the columnIndicies
+     * array and saves them in values beginning at startingIndex, skipping a slot
+     * for each value. If columnIndicies has length 3 and startingIndex is 1, the
+     * values will be stored in slots 1, 3, and 5.
+     * @param values the String[] to populate
+     * @param cursor the cursor from which to read
+     * @param columnIndicies the indicies of the values to read from the cursor
+     * @param startingIndex the slot in which to start storing values, and must be either 0 or 1.
+     */
+    private static void populateValues(String[] values, Cursor cursor, int[] columnIndicies,
+            int startingIndex) {
+        assert startingIndex == 0 || startingIndex == 1;
+        for (int i = 0; i < columnIndicies.length; i++) {
+            values[startingIndex + i*2] = cursor.getString(columnIndicies[i]);
+        }
+    }
+
+    /**
+     * Increment the cursors past the rows indicated in the most recent call to next().
+     * This will only have an affect once per call to next().
+     */
+    private void incrementCursors() {
+        if (mCompareResultIsValid) {
+            switch (mCompareResult) {
+                case LEFT:
+                    mCursorLeft.moveToNext();
+                    break;
+                case RIGHT:
+                    mCursorRight.moveToNext();
+                    break;
+                case BOTH:
+                    mCursorLeft.moveToNext();
+                    mCursorRight.moveToNext();
+                    break;
+            }
+            mCompareResultIsValid = false;
+        }
+    }
+
+    /**
+     * Compare the values. Values contains n pairs of strings. If all the pairs of strings match
+     * then returns 0. Otherwise returns the comparison result of the first non-matching pair
+     * of values, -1 if the first of the pair is less than the second of the pair or 1 if it
+     * is greater.
+     * @param values the n pairs of values to compare
+     * @return -1, 0, or 1 as described above.
+     */
+    private static int compareStrings(String... values) {
+        if ((values.length % 2) != 0) {
+            throw new IllegalArgumentException("you must specify an even number of values");
+        }
+
+        for (int index = 0; index < values.length; index+=2) {
+            if (values[index] == null) {
+                if (values[index+1] == null) continue;
+                return -1;
+            }
+
+            if (values[index+1] == null) {
+                return 1;
+            }
+
+            int comp = values[index].compareTo(values[index+1]);
+            if (comp != 0) {
+                return comp < 0 ? -1 : 1;
+            }
+        }
+
+        return 0;
+    }
+}
diff --git a/android/database/CursorToBulkCursorAdaptor.java b/android/database/CursorToBulkCursorAdaptor.java
new file mode 100644
index 0000000..ce86807
--- /dev/null
+++ b/android/database/CursorToBulkCursorAdaptor.java
@@ -0,0 +1,298 @@
+/*
+ * 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 android.database;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.content.ContentResolver.NotifyFlags;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Wraps a BulkCursor around an existing Cursor making it remotable.
+ * <p>
+ * If the wrapped cursor returns non-null from {@link CrossProcessCursor#getWindow}
+ * then it is assumed to own the window.  Otherwise, the adaptor provides a
+ * window to be filled and ensures it gets closed as needed during deactivation
+ * and requeries.
+ * </p>
+ *
+ * {@hide}
+ */
+public final class CursorToBulkCursorAdaptor extends BulkCursorNative
+        implements IBinder.DeathRecipient {
+    private static final String TAG = "Cursor";
+
+    private final Object mLock = new Object();
+    private final String mProviderName;
+    private ContentObserverProxy mObserver;
+
+    /**
+     * The cursor that is being adapted.
+     * This field is set to null when the cursor is closed.
+     */
+    private CrossProcessCursor mCursor;
+
+    /**
+     * The cursor window that was filled by the cross process cursor in the
+     * case where the cursor does not support getWindow.
+     * This field is only ever non-null when the window has actually be filled.
+     */
+    private CursorWindow mFilledWindow;
+
+    private static final class ContentObserverProxy extends ContentObserver {
+        protected IContentObserver mRemote;
+
+        public ContentObserverProxy(IContentObserver remoteObserver, DeathRecipient recipient) {
+            super(null);
+            mRemote = remoteObserver;
+            try {
+                remoteObserver.asBinder().linkToDeath(recipient, 0);
+            } catch (RemoteException e) {
+                // Do nothing, the far side is dead
+            }
+        }
+
+        public boolean unlinkToDeath(DeathRecipient recipient) {
+            return mRemote.asBinder().unlinkToDeath(recipient, 0);
+        }
+
+        @Override
+        public boolean deliverSelfNotifications() {
+            // The far side handles the self notifications.
+            return false;
+        }
+
+        @Override
+        public void onChange(boolean selfChange, @NonNull Collection<Uri> uris,
+                @NotifyFlags int flags, @UserIdInt int userId) {
+            // Since we deliver changes from the most-specific to least-specific
+            // overloads, we only need to redirect from the most-specific local
+            // method to the most-specific remote method
+
+            final ArrayList<Uri> asList = new ArrayList<>();
+            uris.forEach(asList::add);
+            final Uri[] asArray = asList.toArray(new Uri[asList.size()]);
+
+            try {
+                mRemote.onChangeEtc(selfChange, asArray, flags, userId);
+            } catch (RemoteException ex) {
+                // Do nothing, the far side is dead
+            }
+        }
+    }
+
+    public CursorToBulkCursorAdaptor(Cursor cursor, IContentObserver observer,
+            String providerName) {
+        if (cursor instanceof CrossProcessCursor) {
+            mCursor = (CrossProcessCursor)cursor;
+        } else {
+            mCursor = new CrossProcessCursorWrapper(cursor);
+        }
+        mProviderName = providerName;
+
+        synchronized (mLock) {
+            createAndRegisterObserverProxyLocked(observer);
+        }
+    }
+
+    private void closeFilledWindowLocked() {
+        if (mFilledWindow != null) {
+            mFilledWindow.close();
+            mFilledWindow = null;
+        }
+    }
+
+    private void disposeLocked() {
+        if (mCursor != null) {
+            unregisterObserverProxyLocked();
+            mCursor.close();
+            mCursor = null;
+        }
+
+        closeFilledWindowLocked();
+    }
+
+    private void throwIfCursorIsClosed() {
+        if (mCursor == null) {
+            throw new StaleDataException("Attempted to access a cursor after it has been closed.");
+        }
+    }
+
+    @Override
+    public void binderDied() {
+        synchronized (mLock) {
+            disposeLocked();
+        }
+    }
+
+    /**
+     * Returns an object that contains sufficient metadata to reconstruct
+     * the cursor remotely.  May throw if an error occurs when executing the query
+     * and obtaining the row count.
+     */
+    public BulkCursorDescriptor getBulkCursorDescriptor() {
+        synchronized (mLock) {
+            throwIfCursorIsClosed();
+
+            BulkCursorDescriptor d = new BulkCursorDescriptor();
+            d.cursor = this;
+            d.columnNames = mCursor.getColumnNames();
+            d.wantsAllOnMoveCalls = mCursor.getWantsAllOnMoveCalls();
+            d.count = mCursor.getCount();
+            d.window = mCursor.getWindow();
+            if (d.window != null) {
+                // Acquire a reference to the window because its reference count will be
+                // decremented when it is returned as part of the binder call reply parcel.
+                d.window.acquireReference();
+            }
+            return d;
+        }
+    }
+
+    @Override
+    public CursorWindow getWindow(int position) {
+        synchronized (mLock) {
+            throwIfCursorIsClosed();
+
+            if (!mCursor.moveToPosition(position)) {
+                closeFilledWindowLocked();
+                return null;
+            }
+
+            CursorWindow window = mCursor.getWindow();
+            if (window != null) {
+                closeFilledWindowLocked();
+            } else {
+                window = mFilledWindow;
+                if (window == null) {
+                    mFilledWindow = new CursorWindow(mProviderName);
+                    window = mFilledWindow;
+                } else if (position < window.getStartPosition()
+                        || position >= window.getStartPosition() + window.getNumRows()) {
+                    window.clear();
+                }
+                mCursor.fillWindow(position, window);
+            }
+
+            if (window != null) {
+                // Acquire a reference to the window because its reference count will be
+                // decremented when it is returned as part of the binder call reply parcel.
+                window.acquireReference();
+            }
+            return window;
+        }
+    }
+
+    @Override
+    public void onMove(int position) {
+        synchronized (mLock) {
+            throwIfCursorIsClosed();
+
+            mCursor.onMove(mCursor.getPosition(), position);
+        }
+    }
+
+    @Override
+    public void deactivate() {
+        synchronized (mLock) {
+            if (mCursor != null) {
+                unregisterObserverProxyLocked();
+                mCursor.deactivate();
+            }
+
+            closeFilledWindowLocked();
+        }
+    }
+
+    @Override
+    public void close() {
+        synchronized (mLock) {
+            disposeLocked();
+        }
+    }
+
+    @Override
+    public int requery(IContentObserver observer) {
+        synchronized (mLock) {
+            throwIfCursorIsClosed();
+
+            closeFilledWindowLocked();
+
+            try {
+                if (!mCursor.requery()) {
+                    return -1;
+                }
+            } catch (IllegalStateException e) {
+                IllegalStateException leakProgram = new IllegalStateException(
+                        mProviderName + " Requery misuse db, mCursor isClosed:" +
+                        mCursor.isClosed(), e);
+                throw leakProgram;
+            }
+
+            unregisterObserverProxyLocked();
+            createAndRegisterObserverProxyLocked(observer);
+            return mCursor.getCount();
+        }
+    }
+
+    /**
+     * Create a ContentObserver from the observer and register it as an observer on the
+     * underlying cursor.
+     * @param observer the IContentObserver that wants to monitor the cursor
+     * @throws IllegalStateException if an observer is already registered
+     */
+    private void createAndRegisterObserverProxyLocked(IContentObserver observer) {
+        if (mObserver != null) {
+            throw new IllegalStateException("an observer is already registered");
+        }
+        mObserver = new ContentObserverProxy(observer, this);
+        mCursor.registerContentObserver(mObserver);
+    }
+
+    /** Unregister the observer if it is already registered. */
+    private void unregisterObserverProxyLocked() {
+        if (mObserver != null) {
+            mCursor.unregisterContentObserver(mObserver);
+            mObserver.unlinkToDeath(this);
+            mObserver = null;
+        }
+    }
+
+    @Override
+    public Bundle getExtras() {
+        synchronized (mLock) {
+            throwIfCursorIsClosed();
+
+            return mCursor.getExtras();
+        }
+    }
+
+    @Override
+    public Bundle respond(Bundle extras) {
+        synchronized (mLock) {
+            throwIfCursorIsClosed();
+
+            return mCursor.respond(extras);
+        }
+    }
+}
diff --git a/android/database/CursorWindow.java b/android/database/CursorWindow.java
new file mode 100644
index 0000000..063a2d0
--- /dev/null
+++ b/android/database/CursorWindow.java
@@ -0,0 +1,823 @@
+/*
+ * 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 android.database;
+
+import android.annotation.BytesLong;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.res.Resources;
+import android.database.sqlite.SQLiteClosable;
+import android.database.sqlite.SQLiteException;
+import android.os.Binder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Process;
+import android.util.Log;
+import android.util.LongSparseArray;
+import android.util.SparseIntArray;
+
+import dalvik.annotation.optimization.FastNative;
+import dalvik.system.CloseGuard;
+
+/**
+ * A buffer containing multiple cursor rows.
+ * <p>
+ * A {@link CursorWindow} is read-write when initially created and used locally.
+ * When sent to a remote process (by writing it to a {@link Parcel}), the remote process
+ * receives a read-only view of the cursor window.  Typically the cursor window
+ * will be allocated by the producer, filled with data, and then sent to the
+ * consumer for reading.
+ * </p>
+ */
+public class CursorWindow extends SQLiteClosable implements Parcelable {
+    private static final String STATS_TAG = "CursorWindowStats";
+
+    // This static member will be evaluated when first used.
+    @UnsupportedAppUsage
+    private static int sCursorWindowSize = -1;
+
+    /**
+     * The native CursorWindow object pointer.  (FOR INTERNAL USE ONLY)
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public long mWindowPtr;
+
+    private int mStartPos;
+    private final String mName;
+
+    private final CloseGuard mCloseGuard = CloseGuard.get();
+
+    // May throw CursorWindowAllocationException
+    private static native long nativeCreate(String name, int cursorWindowSize);
+
+    // May throw CursorWindowAllocationException
+    private static native long nativeCreateFromParcel(Parcel parcel);
+    private static native void nativeDispose(long windowPtr);
+    private static native void nativeWriteToParcel(long windowPtr, Parcel parcel);
+
+    private static native String nativeGetName(long windowPtr);
+    private static native byte[] nativeGetBlob(long windowPtr, int row, int column);
+    private static native String nativeGetString(long windowPtr, int row, int column);
+    private static native void nativeCopyStringToBuffer(long windowPtr, int row, int column,
+            CharArrayBuffer buffer);
+    private static native boolean nativePutBlob(long windowPtr, byte[] value, int row, int column);
+    private static native boolean nativePutString(long windowPtr, String value,
+            int row, int column);
+
+    // Below native methods don't do unconstrained work, so are FastNative for performance
+
+    @FastNative
+    private static native void nativeClear(long windowPtr);
+
+    @FastNative
+    private static native int nativeGetNumRows(long windowPtr);
+    @FastNative
+    private static native boolean nativeSetNumColumns(long windowPtr, int columnNum);
+    @FastNative
+    private static native boolean nativeAllocRow(long windowPtr);
+    @FastNative
+    private static native void nativeFreeLastRow(long windowPtr);
+
+    @FastNative
+    private static native int nativeGetType(long windowPtr, int row, int column);
+    @FastNative
+    private static native long nativeGetLong(long windowPtr, int row, int column);
+    @FastNative
+    private static native double nativeGetDouble(long windowPtr, int row, int column);
+
+    @FastNative
+    private static native boolean nativePutLong(long windowPtr, long value, int row, int column);
+    @FastNative
+    private static native boolean nativePutDouble(long windowPtr, double value, int row, int column);
+    @FastNative
+    private static native boolean nativePutNull(long windowPtr, int row, int column);
+
+
+    /**
+     * Creates a new empty cursor window and gives it a name.
+     * <p>
+     * The cursor initially has no rows or columns.  Call {@link #setNumColumns(int)} to
+     * set the number of columns before adding any rows to the cursor.
+     * </p>
+     *
+     * @param name The name of the cursor window, or null if none.
+     */
+    public CursorWindow(String name) {
+        this(name, getCursorWindowSize());
+    }
+
+    /**
+     * Creates a new empty cursor window and gives it a name.
+     * <p>
+     * The cursor initially has no rows or columns.  Call {@link #setNumColumns(int)} to
+     * set the number of columns before adding any rows to the cursor.
+     * </p>
+     *
+     * @param name The name of the cursor window, or null if none.
+     * @param windowSizeBytes Size of cursor window in bytes.
+     * <p><strong>Note:</strong> Memory is dynamically allocated as data rows are added to the
+     * window. Depending on the amount of data stored, the actual amount of memory allocated can be
+     * lower than specified size, but cannot exceed it.
+     */
+    public CursorWindow(String name, @BytesLong long windowSizeBytes) {
+        mStartPos = 0;
+        mName = name != null && name.length() != 0 ? name : "<unnamed>";
+        mWindowPtr = nativeCreate(mName, (int) windowSizeBytes);
+        if (mWindowPtr == 0) {
+            throw new AssertionError(); // Not possible, the native code won't return it.
+        }
+        mCloseGuard.open("close");
+        recordNewWindow(Binder.getCallingPid(), mWindowPtr);
+    }
+
+    /**
+     * Creates a new empty cursor window.
+     * <p>
+     * The cursor initially has no rows or columns.  Call {@link #setNumColumns(int)} to
+     * set the number of columns before adding any rows to the cursor.
+     * </p>
+     *
+     * @param localWindow True if this window will be used in this process only,
+     * false if it might be sent to another processes.  This argument is ignored.
+     *
+     * @deprecated There is no longer a distinction between local and remote
+     * cursor windows.  Use the {@link #CursorWindow(String)} constructor instead.
+     */
+    @Deprecated
+    public CursorWindow(boolean localWindow) {
+        this((String)null);
+    }
+
+    private CursorWindow(Parcel source) {
+        mStartPos = source.readInt();
+        mWindowPtr = nativeCreateFromParcel(source);
+        if (mWindowPtr == 0) {
+            throw new AssertionError(); // Not possible, the native code won't return it.
+        }
+        mName = nativeGetName(mWindowPtr);
+        mCloseGuard.open("close");
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            if (mCloseGuard != null) {
+                mCloseGuard.warnIfOpen();
+            }
+            dispose();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    private void dispose() {
+        if (mCloseGuard != null) {
+            mCloseGuard.close();
+        }
+        if (mWindowPtr != 0) {
+            recordClosingOfWindow(mWindowPtr);
+            nativeDispose(mWindowPtr);
+            mWindowPtr = 0;
+        }
+    }
+
+    /**
+     * Gets the name of this cursor window, never null.
+     * @hide
+     */
+    public String getName() {
+        return mName;
+    }
+
+    /**
+     * Clears out the existing contents of the window, making it safe to reuse
+     * for new data.
+     * <p>
+     * The start position ({@link #getStartPosition()}), number of rows ({@link #getNumRows()}),
+     * and number of columns in the cursor are all reset to zero.
+     * </p>
+     */
+    public void clear() {
+        acquireReference();
+        try {
+            mStartPos = 0;
+            nativeClear(mWindowPtr);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Gets the start position of this cursor window.
+     * <p>
+     * The start position is the zero-based index of the first row that this window contains
+     * relative to the entire result set of the {@link Cursor}.
+     * </p>
+     *
+     * @return The zero-based start position.
+     */
+    public int getStartPosition() {
+        return mStartPos;
+    }
+
+    /**
+     * Sets the start position of this cursor window.
+     * <p>
+     * The start position is the zero-based index of the first row that this window contains
+     * relative to the entire result set of the {@link Cursor}.
+     * </p>
+     *
+     * @param pos The new zero-based start position.
+     */
+    public void setStartPosition(int pos) {
+        mStartPos = pos;
+    }
+
+    /**
+     * Gets the number of rows in this window.
+     *
+     * @return The number of rows in this cursor window.
+     */
+    public int getNumRows() {
+        acquireReference();
+        try {
+            return nativeGetNumRows(mWindowPtr);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Sets the number of columns in this window.
+     * <p>
+     * This method must be called before any rows are added to the window, otherwise
+     * it will fail to set the number of columns if it differs from the current number
+     * of columns.
+     * </p>
+     *
+     * @param columnNum The new number of columns.
+     * @return True if successful.
+     */
+    public boolean setNumColumns(int columnNum) {
+        acquireReference();
+        try {
+            return nativeSetNumColumns(mWindowPtr, columnNum);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Allocates a new row at the end of this cursor window.
+     *
+     * @return True if successful, false if the cursor window is out of memory.
+     */
+    public boolean allocRow(){
+        acquireReference();
+        try {
+            return nativeAllocRow(mWindowPtr);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Frees the last row in this cursor window.
+     */
+    public void freeLastRow(){
+        acquireReference();
+        try {
+            nativeFreeLastRow(mWindowPtr);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Returns true if the field at the specified row and column index
+     * has type {@link Cursor#FIELD_TYPE_NULL}.
+     *
+     * @param row The zero-based row index.
+     * @param column The zero-based column index.
+     * @return True if the field has type {@link Cursor#FIELD_TYPE_NULL}.
+     * @deprecated Use {@link #getType(int, int)} instead.
+     */
+    @Deprecated
+    public boolean isNull(int row, int column) {
+        return getType(row, column) == Cursor.FIELD_TYPE_NULL;
+    }
+
+    /**
+     * Returns true if the field at the specified row and column index
+     * has type {@link Cursor#FIELD_TYPE_BLOB} or {@link Cursor#FIELD_TYPE_NULL}.
+     *
+     * @param row The zero-based row index.
+     * @param column The zero-based column index.
+     * @return True if the field has type {@link Cursor#FIELD_TYPE_BLOB} or
+     * {@link Cursor#FIELD_TYPE_NULL}.
+     * @deprecated Use {@link #getType(int, int)} instead.
+     */
+    @Deprecated
+    public boolean isBlob(int row, int column) {
+        int type = getType(row, column);
+        return type == Cursor.FIELD_TYPE_BLOB || type == Cursor.FIELD_TYPE_NULL;
+    }
+
+    /**
+     * Returns true if the field at the specified row and column index
+     * has type {@link Cursor#FIELD_TYPE_INTEGER}.
+     *
+     * @param row The zero-based row index.
+     * @param column The zero-based column index.
+     * @return True if the field has type {@link Cursor#FIELD_TYPE_INTEGER}.
+     * @deprecated Use {@link #getType(int, int)} instead.
+     */
+    @Deprecated
+    public boolean isLong(int row, int column) {
+        return getType(row, column) == Cursor.FIELD_TYPE_INTEGER;
+    }
+
+    /**
+     * Returns true if the field at the specified row and column index
+     * has type {@link Cursor#FIELD_TYPE_FLOAT}.
+     *
+     * @param row The zero-based row index.
+     * @param column The zero-based column index.
+     * @return True if the field has type {@link Cursor#FIELD_TYPE_FLOAT}.
+     * @deprecated Use {@link #getType(int, int)} instead.
+     */
+    @Deprecated
+    public boolean isFloat(int row, int column) {
+        return getType(row, column) == Cursor.FIELD_TYPE_FLOAT;
+    }
+
+    /**
+     * Returns true if the field at the specified row and column index
+     * has type {@link Cursor#FIELD_TYPE_STRING} or {@link Cursor#FIELD_TYPE_NULL}.
+     *
+     * @param row The zero-based row index.
+     * @param column The zero-based column index.
+     * @return True if the field has type {@link Cursor#FIELD_TYPE_STRING}
+     * or {@link Cursor#FIELD_TYPE_NULL}.
+     * @deprecated Use {@link #getType(int, int)} instead.
+     */
+    @Deprecated
+    public boolean isString(int row, int column) {
+        int type = getType(row, column);
+        return type == Cursor.FIELD_TYPE_STRING || type == Cursor.FIELD_TYPE_NULL;
+    }
+
+    /**
+     * Returns the type of the field at the specified row and column index.
+     * <p>
+     * The returned field types are:
+     * <ul>
+     * <li>{@link Cursor#FIELD_TYPE_NULL}</li>
+     * <li>{@link Cursor#FIELD_TYPE_INTEGER}</li>
+     * <li>{@link Cursor#FIELD_TYPE_FLOAT}</li>
+     * <li>{@link Cursor#FIELD_TYPE_STRING}</li>
+     * <li>{@link Cursor#FIELD_TYPE_BLOB}</li>
+     * </ul>
+     * </p>
+     *
+     * @param row The zero-based row index.
+     * @param column The zero-based column index.
+     * @return The field type.
+     */
+    public int getType(int row, int column) {
+        acquireReference();
+        try {
+            return nativeGetType(mWindowPtr, row - mStartPos, column);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Gets the value of the field at the specified row and column index as a byte array.
+     * <p>
+     * The result is determined as follows:
+     * <ul>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_NULL}, then the result
+     * is <code>null</code>.</li>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_BLOB}, then the result
+     * is the blob value.</li>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_STRING}, then the result
+     * is the array of bytes that make up the internal representation of the
+     * string value.</li>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_INTEGER} or
+     * {@link Cursor#FIELD_TYPE_FLOAT}, then a {@link SQLiteException} is thrown.</li>
+     * </ul>
+     * </p>
+     *
+     * @param row The zero-based row index.
+     * @param column The zero-based column index.
+     * @return The value of the field as a byte array.
+     */
+    public byte[] getBlob(int row, int column) {
+        acquireReference();
+        try {
+            return nativeGetBlob(mWindowPtr, row - mStartPos, column);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Gets the value of the field at the specified row and column index as a string.
+     * <p>
+     * The result is determined as follows:
+     * <ul>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_NULL}, then the result
+     * is <code>null</code>.</li>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_STRING}, then the result
+     * is the string value.</li>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_INTEGER}, then the result
+     * is a string representation of the integer in decimal, obtained by formatting the
+     * value with the <code>printf</code> family of functions using
+     * format specifier <code>%lld</code>.</li>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_FLOAT}, then the result
+     * is a string representation of the floating-point value in decimal, obtained by
+     * formatting the value with the <code>printf</code> family of functions using
+     * format specifier <code>%g</code>.</li>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_BLOB}, then a
+     * {@link SQLiteException} is thrown.</li>
+     * </ul>
+     * </p>
+     *
+     * @param row The zero-based row index.
+     * @param column The zero-based column index.
+     * @return The value of the field as a string.
+     */
+    public String getString(int row, int column) {
+        acquireReference();
+        try {
+            return nativeGetString(mWindowPtr, row - mStartPos, column);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Copies the text of the field at the specified row and column index into
+     * a {@link CharArrayBuffer}.
+     * <p>
+     * The buffer is populated as follows:
+     * <ul>
+     * <li>If the buffer is too small for the value to be copied, then it is
+     * automatically resized.</li>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_NULL}, then the buffer
+     * is set to an empty string.</li>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_STRING}, then the buffer
+     * is set to the contents of the string.</li>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_INTEGER}, then the buffer
+     * is set to a string representation of the integer in decimal, obtained by formatting the
+     * value with the <code>printf</code> family of functions using
+     * format specifier <code>%lld</code>.</li>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_FLOAT}, then the buffer is
+     * set to a string representation of the floating-point value in decimal, obtained by
+     * formatting the value with the <code>printf</code> family of functions using
+     * format specifier <code>%g</code>.</li>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_BLOB}, then a
+     * {@link SQLiteException} is thrown.</li>
+     * </ul>
+     * </p>
+     *
+     * @param row The zero-based row index.
+     * @param column The zero-based column index.
+     * @param buffer The {@link CharArrayBuffer} to hold the string.  It is automatically
+     * resized if the requested string is larger than the buffer's current capacity.
+      */
+    public void copyStringToBuffer(int row, int column, CharArrayBuffer buffer) {
+        if (buffer == null) {
+            throw new IllegalArgumentException("CharArrayBuffer should not be null");
+        }
+        acquireReference();
+        try {
+            nativeCopyStringToBuffer(mWindowPtr, row - mStartPos, column, buffer);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Gets the value of the field at the specified row and column index as a <code>long</code>.
+     * <p>
+     * The result is determined as follows:
+     * <ul>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_NULL}, then the result
+     * is <code>0L</code>.</li>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_STRING}, then the result
+     * is the value obtained by parsing the string value with <code>strtoll</code>.
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_INTEGER}, then the result
+     * is the <code>long</code> value.</li>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_FLOAT}, then the result
+     * is the floating-point value converted to a <code>long</code>.</li>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_BLOB}, then a
+     * {@link SQLiteException} is thrown.</li>
+     * </ul>
+     * </p>
+     *
+     * @param row The zero-based row index.
+     * @param column The zero-based column index.
+     * @return The value of the field as a <code>long</code>.
+     */
+    public long getLong(int row, int column) {
+        acquireReference();
+        try {
+            return nativeGetLong(mWindowPtr, row - mStartPos, column);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Gets the value of the field at the specified row and column index as a
+     * <code>double</code>.
+     * <p>
+     * The result is determined as follows:
+     * <ul>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_NULL}, then the result
+     * is <code>0.0</code>.</li>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_STRING}, then the result
+     * is the value obtained by parsing the string value with <code>strtod</code>.
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_INTEGER}, then the result
+     * is the integer value converted to a <code>double</code>.</li>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_FLOAT}, then the result
+     * is the <code>double</code> value.</li>
+     * <li>If the field is of type {@link Cursor#FIELD_TYPE_BLOB}, then a
+     * {@link SQLiteException} is thrown.</li>
+     * </ul>
+     * </p>
+     *
+     * @param row The zero-based row index.
+     * @param column The zero-based column index.
+     * @return The value of the field as a <code>double</code>.
+     */
+    public double getDouble(int row, int column) {
+        acquireReference();
+        try {
+            return nativeGetDouble(mWindowPtr, row - mStartPos, column);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Gets the value of the field at the specified row and column index as a
+     * <code>short</code>.
+     * <p>
+     * The result is determined by invoking {@link #getLong} and converting the
+     * result to <code>short</code>.
+     * </p>
+     *
+     * @param row The zero-based row index.
+     * @param column The zero-based column index.
+     * @return The value of the field as a <code>short</code>.
+     */
+    public short getShort(int row, int column) {
+        return (short) getLong(row, column);
+    }
+
+    /**
+     * Gets the value of the field at the specified row and column index as an
+     * <code>int</code>.
+     * <p>
+     * The result is determined by invoking {@link #getLong} and converting the
+     * result to <code>int</code>.
+     * </p>
+     *
+     * @param row The zero-based row index.
+     * @param column The zero-based column index.
+     * @return The value of the field as an <code>int</code>.
+     */
+    public int getInt(int row, int column) {
+        return (int) getLong(row, column);
+    }
+
+    /**
+     * Gets the value of the field at the specified row and column index as a
+     * <code>float</code>.
+     * <p>
+     * The result is determined by invoking {@link #getDouble} and converting the
+     * result to <code>float</code>.
+     * </p>
+     *
+     * @param row The zero-based row index.
+     * @param column The zero-based column index.
+     * @return The value of the field as an <code>float</code>.
+     */
+    public float getFloat(int row, int column) {
+        return (float) getDouble(row, column);
+    }
+
+    /**
+     * Copies a byte array into the field at the specified row and column index.
+     *
+     * @param value The value to store.
+     * @param row The zero-based row index.
+     * @param column The zero-based column index.
+     * @return True if successful.
+     */
+    public boolean putBlob(byte[] value, int row, int column) {
+        acquireReference();
+        try {
+            return nativePutBlob(mWindowPtr, value, row - mStartPos, column);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Copies a string into the field at the specified row and column index.
+     *
+     * @param value The value to store.
+     * @param row The zero-based row index.
+     * @param column The zero-based column index.
+     * @return True if successful.
+     */
+    public boolean putString(String value, int row, int column) {
+        acquireReference();
+        try {
+            return nativePutString(mWindowPtr, value, row - mStartPos, column);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Puts a long integer into the field at the specified row and column index.
+     *
+     * @param value The value to store.
+     * @param row The zero-based row index.
+     * @param column The zero-based column index.
+     * @return True if successful.
+     */
+    public boolean putLong(long value, int row, int column) {
+        acquireReference();
+        try {
+            return nativePutLong(mWindowPtr, value, row - mStartPos, column);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Puts a double-precision floating point value into the field at the
+     * specified row and column index.
+     *
+     * @param value The value to store.
+     * @param row The zero-based row index.
+     * @param column The zero-based column index.
+     * @return True if successful.
+     */
+    public boolean putDouble(double value, int row, int column) {
+        acquireReference();
+        try {
+            return nativePutDouble(mWindowPtr, value, row - mStartPos, column);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Puts a null value into the field at the specified row and column index.
+     *
+     * @param row The zero-based row index.
+     * @param column The zero-based column index.
+     * @return True if successful.
+     */
+    public boolean putNull(int row, int column) {
+        acquireReference();
+        try {
+            return nativePutNull(mWindowPtr, row - mStartPos, column);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    public static final @android.annotation.NonNull Parcelable.Creator<CursorWindow> CREATOR
+            = new Parcelable.Creator<CursorWindow>() {
+        public CursorWindow createFromParcel(Parcel source) {
+            return new CursorWindow(source);
+        }
+
+        public CursorWindow[] newArray(int size) {
+            return new CursorWindow[size];
+        }
+    };
+
+    public static CursorWindow newFromParcel(Parcel p) {
+        return CREATOR.createFromParcel(p);
+    }
+
+    public int describeContents() {
+        return 0;
+    }
+
+    public void writeToParcel(Parcel dest, int flags) {
+        acquireReference();
+        try {
+            dest.writeInt(mStartPos);
+            nativeWriteToParcel(mWindowPtr, dest);
+        } finally {
+            releaseReference();
+        }
+
+        if ((flags & Parcelable.PARCELABLE_WRITE_RETURN_VALUE) != 0) {
+            releaseReference();
+        }
+    }
+
+    @Override
+    protected void onAllReferencesReleased() {
+        dispose();
+    }
+
+    @UnsupportedAppUsage
+    private static final LongSparseArray<Integer> sWindowToPidMap = new LongSparseArray<Integer>();
+
+    private void recordNewWindow(int pid, long window) {
+        synchronized (sWindowToPidMap) {
+            sWindowToPidMap.put(window, pid);
+            if (Log.isLoggable(STATS_TAG, Log.VERBOSE)) {
+                Log.i(STATS_TAG, "Created a new Cursor. " + printStats());
+            }
+        }
+    }
+
+    private void recordClosingOfWindow(long window) {
+        synchronized (sWindowToPidMap) {
+            if (sWindowToPidMap.size() == 0) {
+                // this means we are not in the ContentProvider.
+                return;
+            }
+            sWindowToPidMap.delete(window);
+        }
+    }
+
+    @UnsupportedAppUsage
+    private String printStats() {
+        StringBuilder buff = new StringBuilder();
+        int myPid = Process.myPid();
+        int total = 0;
+        SparseIntArray pidCounts = new SparseIntArray();
+        synchronized (sWindowToPidMap) {
+            int size = sWindowToPidMap.size();
+            if (size == 0) {
+                // this means we are not in the ContentProvider.
+                return "";
+            }
+            for (int indx = 0; indx < size; indx++) {
+                int pid = sWindowToPidMap.valueAt(indx);
+                int value = pidCounts.get(pid);
+                pidCounts.put(pid, ++value);
+            }
+        }
+        int numPids = pidCounts.size();
+        for (int i = 0; i < numPids;i++) {
+            buff.append(" (# cursors opened by ");
+            int pid = pidCounts.keyAt(i);
+            if (pid == myPid) {
+                buff.append("this proc=");
+            } else {
+                buff.append("pid " + pid + "=");
+            }
+            int num = pidCounts.get(pid);
+            buff.append(num + ")");
+            total += num;
+        }
+        // limit the returned string size to 1000
+        String s = (buff.length() > 980) ? buff.substring(0, 980) : buff.toString();
+        return "# Open Cursors=" + total + s;
+    }
+
+    private static int getCursorWindowSize() {
+        if (sCursorWindowSize < 0) {
+            // The cursor window size. resource xml file specifies the value in kB.
+            // convert it to bytes here by multiplying with 1024.
+            sCursorWindowSize = Resources.getSystem().getInteger(
+                    com.android.internal.R.integer.config_cursorWindowSize) * 1024;
+        }
+        return sCursorWindowSize;
+    }
+
+    @Override
+    public String toString() {
+        return getName() + " {" + Long.toHexString(mWindowPtr) + "}";
+    }
+}
diff --git a/android/database/CursorWindowAllocationException.java b/android/database/CursorWindowAllocationException.java
new file mode 100644
index 0000000..2e3227d
--- /dev/null
+++ b/android/database/CursorWindowAllocationException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2010 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 android.database;
+
+/**
+ * This exception is thrown when a CursorWindow couldn't be allocated,
+ * most probably due to memory not being available.
+ *
+ * @hide
+ */
+public class CursorWindowAllocationException extends RuntimeException {
+    public CursorWindowAllocationException(String description) {
+        super(description);
+    }
+}
diff --git a/android/database/CursorWindowPerfTest.java b/android/database/CursorWindowPerfTest.java
new file mode 100644
index 0000000..c5ef80d
--- /dev/null
+++ b/android/database/CursorWindowPerfTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2017 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 android.database;
+
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteCursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class CursorWindowPerfTest {
+    @Rule
+    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+    private static Context getContext() {
+        return InstrumentationRegistry.getTargetContext();
+    }
+
+    private static final String DB_NAME = CursorWindowPerfTest.class.toString();
+
+    private static SQLiteDatabase sDatabase;
+
+    @BeforeClass
+    public static void setup() {
+        getContext().deleteDatabase(DB_NAME);
+        sDatabase = getContext().openOrCreateDatabase(DB_NAME, Context.MODE_PRIVATE, null);
+
+        for (TableHelper helper : TableHelper.TABLE_HELPERS) {
+            sDatabase.execSQL(helper.createSql());
+            final String insert = helper.insertSql();
+
+            // this test only needs 1 row
+            sDatabase.execSQL(insert, helper.createItem(0));
+        }
+
+    }
+
+    @AfterClass
+    public static void teardown() {
+        getContext().deleteDatabase(DB_NAME);
+    }
+
+    @Test
+    public void loadInt() {
+        loadRowFromCursorWindow(TableHelper.INT_1, false);
+    }
+
+    @Test
+    public void loadInt_doubleRef() {
+        loadRowFromCursorWindow(TableHelper.INT_1, true);
+    }
+
+    @Test
+    public void load10Ints() {
+        loadRowFromCursorWindow(TableHelper.INT_10, false);
+    }
+
+    @Test
+    public void loadUser() {
+        loadRowFromCursorWindow(TableHelper.USER, false);
+    }
+
+    private void loadRowFromCursorWindow(TableHelper helper, boolean doubleRef) {
+        try (Cursor cursor = sDatabase.rawQuery(helper.readSql(), new String[0])) {
+            TableHelper.CursorReader reader = helper.createReader(cursor);
+
+            SQLiteCursor sqLiteCursor = (SQLiteCursor) cursor;
+
+            sqLiteCursor.getCount(); // load one window
+            CursorWindow window = sqLiteCursor.getWindow();
+            assertTrue("must have enough rows", window.getNumRows() >= 1);
+            int start = window.getStartPosition();
+
+            BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+            if (!doubleRef) {
+                // normal access
+                while (state.keepRunning()) {
+                    cursor.moveToPosition(start);
+                    reader.read();
+                }
+            } else {
+                // add an extra window acquire/release to measure overhead
+                while (state.keepRunning()) {
+                    cursor.moveToPosition(start);
+                    try {
+                        window.acquireReference();
+                        reader.read();
+                    } finally {
+                        window.releaseReference();
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/android/database/CursorWrapper.java b/android/database/CursorWrapper.java
new file mode 100644
index 0000000..4496f80
--- /dev/null
+++ b/android/database/CursorWrapper.java
@@ -0,0 +1,270 @@
+/*
+ * 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 android.database;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.os.Bundle;
+
+import java.util.List;
+
+/**
+ * Wrapper class for Cursor that delegates all calls to the actual cursor object.  The primary
+ * use for this class is to extend a cursor while overriding only a subset of its methods.
+ */
+public class CursorWrapper implements Cursor {
+    /** @hide */
+    @UnsupportedAppUsage
+    protected final Cursor mCursor;
+
+    /**
+     * Creates a cursor wrapper.
+     * @param cursor The underlying cursor to wrap.
+     */
+    public CursorWrapper(Cursor cursor) {
+        mCursor = cursor;
+    }
+
+    /**
+     * Gets the underlying cursor that is wrapped by this instance.
+     *
+     * @return The wrapped cursor.
+     */
+    public Cursor getWrappedCursor() {
+        return mCursor;
+    }
+
+    @Override
+    public void close() {
+        mCursor.close(); 
+    }
+ 
+    @Override
+    public boolean isClosed() {
+        return mCursor.isClosed();
+    }
+
+    @Override
+    public int getCount() {
+        return mCursor.getCount();
+    }
+
+    @Override
+    @Deprecated
+    public void deactivate() {
+        mCursor.deactivate();
+    }
+
+    @Override
+    public boolean moveToFirst() {
+        return mCursor.moveToFirst();
+    }
+
+    @Override
+    public int getColumnCount() {
+        return mCursor.getColumnCount();
+    }
+
+    @Override
+    public int getColumnIndex(String columnName) {
+        return mCursor.getColumnIndex(columnName);
+    }
+
+    @Override
+    public int getColumnIndexOrThrow(String columnName)
+            throws IllegalArgumentException {
+        return mCursor.getColumnIndexOrThrow(columnName);
+    }
+
+    @Override
+    public String getColumnName(int columnIndex) {
+         return mCursor.getColumnName(columnIndex);
+    }
+
+    @Override
+    public String[] getColumnNames() {
+        return mCursor.getColumnNames();
+    }
+
+    @Override
+    public double getDouble(int columnIndex) {
+        return mCursor.getDouble(columnIndex);
+    }
+
+    @Override
+    public void setExtras(Bundle extras) {
+        mCursor.setExtras(extras);
+    }
+
+    @Override
+    public Bundle getExtras() {
+        return mCursor.getExtras();
+    }
+
+    @Override
+    public float getFloat(int columnIndex) {
+        return mCursor.getFloat(columnIndex);
+    }
+
+    @Override
+    public int getInt(int columnIndex) {
+        return mCursor.getInt(columnIndex);
+    }
+
+    @Override
+    public long getLong(int columnIndex) {
+        return mCursor.getLong(columnIndex);
+    }
+
+    @Override
+    public short getShort(int columnIndex) {
+        return mCursor.getShort(columnIndex);
+    }
+
+    @Override
+    public String getString(int columnIndex) {
+        return mCursor.getString(columnIndex);
+    }
+    
+    @Override
+    public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
+        mCursor.copyStringToBuffer(columnIndex, buffer);
+    }
+
+    @Override
+    public byte[] getBlob(int columnIndex) {
+        return mCursor.getBlob(columnIndex);
+    }
+    
+    @Override
+    public boolean getWantsAllOnMoveCalls() {
+        return mCursor.getWantsAllOnMoveCalls();
+    }
+
+    @Override
+    public boolean isAfterLast() {
+        return mCursor.isAfterLast();
+    }
+
+    @Override
+    public boolean isBeforeFirst() {
+        return mCursor.isBeforeFirst();
+    }
+
+    @Override
+    public boolean isFirst() {
+        return mCursor.isFirst();
+    }
+
+    @Override
+    public boolean isLast() {
+        return mCursor.isLast();
+    }
+
+    @Override
+    public int getType(int columnIndex) {
+        return mCursor.getType(columnIndex);
+    }
+
+    @Override
+    public boolean isNull(int columnIndex) {
+        return mCursor.isNull(columnIndex);
+    }
+
+    @Override
+    public boolean moveToLast() {
+        return mCursor.moveToLast();
+    }
+
+    @Override
+    public boolean move(int offset) {
+        return mCursor.move(offset);
+    }
+
+    @Override
+    public boolean moveToPosition(int position) {
+        return mCursor.moveToPosition(position);
+    }
+
+    @Override
+    public boolean moveToNext() {
+        return mCursor.moveToNext();
+    }
+
+    @Override
+    public int getPosition() {
+        return mCursor.getPosition();
+    }
+
+    @Override
+    public boolean moveToPrevious() {
+        return mCursor.moveToPrevious();
+    }
+
+    @Override
+    public void registerContentObserver(ContentObserver observer) {
+        mCursor.registerContentObserver(observer);
+    }
+
+    @Override
+    public void registerDataSetObserver(DataSetObserver observer) {
+        mCursor.registerDataSetObserver(observer);
+    }
+
+    @Override
+    @Deprecated
+    public boolean requery() {
+        return mCursor.requery();
+    }
+
+    @Override
+    public Bundle respond(Bundle extras) {
+        return mCursor.respond(extras);
+    }
+
+    @Override
+    public void setNotificationUri(ContentResolver cr, Uri uri) {
+        mCursor.setNotificationUri(cr, uri);
+    }
+
+    @Override
+    public void setNotificationUris(ContentResolver cr, List<Uri> uris) {
+        mCursor.setNotificationUris(cr, uris);
+    }
+
+    @Override
+    public Uri getNotificationUri() {
+        return mCursor.getNotificationUri();
+    }
+
+    @Override
+    public List<Uri> getNotificationUris() {
+        return mCursor.getNotificationUris();
+    }
+
+    @Override
+    public void unregisterContentObserver(ContentObserver observer) {
+        mCursor.unregisterContentObserver(observer);
+    }
+
+    @Override
+    public void unregisterDataSetObserver(DataSetObserver observer) {
+        mCursor.unregisterDataSetObserver(observer);
+    }
+}
+
diff --git a/android/database/DataSetObservable.java b/android/database/DataSetObservable.java
new file mode 100644
index 0000000..ca77a13
--- /dev/null
+++ b/android/database/DataSetObservable.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2007 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 android.database;
+
+/**
+ * A specialization of {@link Observable} for {@link DataSetObserver}
+ * that provides methods for sending notifications to a list of
+ * {@link DataSetObserver} objects.
+ */
+public class DataSetObservable extends Observable<DataSetObserver> {
+    /**
+     * Invokes {@link DataSetObserver#onChanged} on each observer.
+     * Called when the contents of the data set have changed.  The recipient
+     * will obtain the new contents the next time it queries the data set.
+     */
+    public void notifyChanged() {
+        synchronized(mObservers) {
+            // since onChanged() is implemented by the app, it could do anything, including
+            // removing itself from {@link mObservers} - and that could cause problems if
+            // an iterator is used on the ArrayList {@link mObservers}.
+            // to avoid such problems, just march thru the list in the reverse order.
+            for (int i = mObservers.size() - 1; i >= 0; i--) {
+                mObservers.get(i).onChanged();
+            }
+        }
+    }
+
+    /**
+     * Invokes {@link DataSetObserver#onInvalidated} on each observer.
+     * Called when the data set is no longer valid and cannot be queried again,
+     * such as when the data set has been closed.
+     */
+    public void notifyInvalidated() {
+        synchronized (mObservers) {
+            for (int i = mObservers.size() - 1; i >= 0; i--) {
+                mObservers.get(i).onInvalidated();
+            }
+        }
+    }
+}
diff --git a/android/database/DataSetObserver.java b/android/database/DataSetObserver.java
new file mode 100644
index 0000000..28616c8
--- /dev/null
+++ b/android/database/DataSetObserver.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2007 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 android.database;
+
+/**
+ * Receives call backs when a data set has been changed, or made invalid. The typically data sets
+ * that are observed are {@link Cursor}s or {@link android.widget.Adapter}s.
+ * DataSetObserver must be implemented by objects which are added to a DataSetObservable.
+ */
+public abstract class DataSetObserver {
+    /**
+     * This method is called when the entire data set has changed,
+     * most likely through a call to {@link Cursor#requery()} on a {@link Cursor}.
+     */
+    public void onChanged() {
+        // Do nothing
+    }
+
+    /**
+     * This method is called when the entire data becomes invalid,
+     * most likely through a call to {@link Cursor#deactivate()} or {@link Cursor#close()} on a
+     * {@link Cursor}.
+     */
+    public void onInvalidated() {
+        // Do nothing
+    }
+}
diff --git a/android/database/DatabaseErrorHandler.java b/android/database/DatabaseErrorHandler.java
new file mode 100644
index 0000000..55ad921
--- /dev/null
+++ b/android/database/DatabaseErrorHandler.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2010 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 android.database;
+
+import android.database.sqlite.SQLiteDatabase;
+
+/**
+ * An interface to let apps define an action to take when database corruption is detected.
+ */
+public interface DatabaseErrorHandler {
+
+    /**
+     * The method invoked when database corruption is detected.
+     * @param dbObj the {@link SQLiteDatabase} object representing the database on which corruption
+     * is detected.
+     */
+    void onCorruption(SQLiteDatabase dbObj);
+}
diff --git a/android/database/DatabaseUtils.java b/android/database/DatabaseUtils.java
new file mode 100644
index 0000000..9b809b8
--- /dev/null
+++ b/android/database/DatabaseUtils.java
@@ -0,0 +1,1601 @@
+/*
+ * 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 android.database;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.sqlite.SQLiteAbortException;
+import android.database.sqlite.SQLiteConstraintException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
+import android.database.sqlite.SQLiteDiskIOException;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteFullException;
+import android.database.sqlite.SQLiteProgram;
+import android.database.sqlite.SQLiteStatement;
+import android.os.Build;
+import android.os.OperationCanceledException;
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.util.ArrayUtils;
+
+import java.io.FileNotFoundException;
+import java.io.PrintStream;
+import java.text.Collator;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Static utility methods for dealing with databases and {@link Cursor}s.
+ */
+public class DatabaseUtils {
+    private static final String TAG = "DatabaseUtils";
+
+    private static final boolean DEBUG = false;
+
+    /** One of the values returned by {@link #getSqlStatementType(String)}. */
+    public static final int STATEMENT_SELECT = 1;
+    /** One of the values returned by {@link #getSqlStatementType(String)}. */
+    public static final int STATEMENT_UPDATE = 2;
+    /** One of the values returned by {@link #getSqlStatementType(String)}. */
+    public static final int STATEMENT_ATTACH = 3;
+    /** One of the values returned by {@link #getSqlStatementType(String)}. */
+    public static final int STATEMENT_BEGIN = 4;
+    /** One of the values returned by {@link #getSqlStatementType(String)}. */
+    public static final int STATEMENT_COMMIT = 5;
+    /** One of the values returned by {@link #getSqlStatementType(String)}. */
+    public static final int STATEMENT_ABORT = 6;
+    /** One of the values returned by {@link #getSqlStatementType(String)}. */
+    public static final int STATEMENT_PRAGMA = 7;
+    /** One of the values returned by {@link #getSqlStatementType(String)}. */
+    public static final int STATEMENT_DDL = 8;
+    /** One of the values returned by {@link #getSqlStatementType(String)}. */
+    public static final int STATEMENT_UNPREPARED = 9;
+    /** One of the values returned by {@link #getSqlStatementType(String)}. */
+    public static final int STATEMENT_OTHER = 99;
+
+    /**
+     * Special function for writing an exception result at the header of
+     * a parcel, to be used when returning an exception from a transaction.
+     * exception will be re-thrown by the function in another process
+     * @param reply Parcel to write to
+     * @param e The Exception to be written.
+     * @see Parcel#writeNoException
+     * @see Parcel#writeException
+     */
+    public static final void writeExceptionToParcel(Parcel reply, Exception e) {
+        int code = 0;
+        boolean logException = true;
+        if (e instanceof FileNotFoundException) {
+            code = 1;
+            logException = false;
+        } else if (e instanceof IllegalArgumentException) {
+            code = 2;
+        } else if (e instanceof UnsupportedOperationException) {
+            code = 3;
+        } else if (e instanceof SQLiteAbortException) {
+            code = 4;
+        } else if (e instanceof SQLiteConstraintException) {
+            code = 5;
+        } else if (e instanceof SQLiteDatabaseCorruptException) {
+            code = 6;
+        } else if (e instanceof SQLiteFullException) {
+            code = 7;
+        } else if (e instanceof SQLiteDiskIOException) {
+            code = 8;
+        } else if (e instanceof SQLiteException) {
+            code = 9;
+        } else if (e instanceof OperationApplicationException) {
+            code = 10;
+        } else if (e instanceof OperationCanceledException) {
+            code = 11;
+            logException = false;
+        } else {
+            reply.writeException(e);
+            Log.e(TAG, "Writing exception to parcel", e);
+            return;
+        }
+        reply.writeInt(code);
+        reply.writeString(e.getMessage());
+
+        if (logException) {
+            Log.e(TAG, "Writing exception to parcel", e);
+        }
+    }
+
+    /**
+     * Special function for reading an exception result from the header of
+     * a parcel, to be used after receiving the result of a transaction.  This
+     * will throw the exception for you if it had been written to the Parcel,
+     * otherwise return and let you read the normal result data from the Parcel.
+     * @param reply Parcel to read from
+     * @see Parcel#writeNoException
+     * @see Parcel#readException
+     */
+    public static final void readExceptionFromParcel(Parcel reply) {
+        int code = reply.readExceptionCode();
+        if (code == 0) return;
+        String msg = reply.readString();
+        DatabaseUtils.readExceptionFromParcel(reply, msg, code);
+    }
+
+    public static void readExceptionWithFileNotFoundExceptionFromParcel(
+            Parcel reply) throws FileNotFoundException {
+        int code = reply.readExceptionCode();
+        if (code == 0) return;
+        String msg = reply.readString();
+        if (code == 1) {
+            throw new FileNotFoundException(msg);
+        } else {
+            DatabaseUtils.readExceptionFromParcel(reply, msg, code);
+        }
+    }
+
+    public static void readExceptionWithOperationApplicationExceptionFromParcel(
+            Parcel reply) throws OperationApplicationException {
+        int code = reply.readExceptionCode();
+        if (code == 0) return;
+        String msg = reply.readString();
+        if (code == 10) {
+            throw new OperationApplicationException(msg);
+        } else {
+            DatabaseUtils.readExceptionFromParcel(reply, msg, code);
+        }
+    }
+
+    private static final void readExceptionFromParcel(Parcel reply, String msg, int code) {
+        switch (code) {
+            case 2:
+                throw new IllegalArgumentException(msg);
+            case 3:
+                throw new UnsupportedOperationException(msg);
+            case 4:
+                throw new SQLiteAbortException(msg);
+            case 5:
+                throw new SQLiteConstraintException(msg);
+            case 6:
+                throw new SQLiteDatabaseCorruptException(msg);
+            case 7:
+                throw new SQLiteFullException(msg);
+            case 8:
+                throw new SQLiteDiskIOException(msg);
+            case 9:
+                throw new SQLiteException(msg);
+            case 11:
+                throw new OperationCanceledException(msg);
+            default:
+                reply.readException(code, msg);
+        }
+    }
+
+    /**
+     * Binds the given Object to the given SQLiteProgram using the proper
+     * typing. For example, bind numbers as longs/doubles, and everything else
+     * as a string by call toString() on it.
+     *
+     * @param prog the program to bind the object to
+     * @param index the 1-based index to bind at
+     * @param value the value to bind
+     */
+    public static void bindObjectToProgram(SQLiteProgram prog, int index,
+            Object value) {
+        if (value == null) {
+            prog.bindNull(index);
+        } else if (value instanceof Double || value instanceof Float) {
+            prog.bindDouble(index, ((Number)value).doubleValue());
+        } else if (value instanceof Number) {
+            prog.bindLong(index, ((Number)value).longValue());
+        } else if (value instanceof Boolean) {
+            Boolean bool = (Boolean)value;
+            if (bool) {
+                prog.bindLong(index, 1);
+            } else {
+                prog.bindLong(index, 0);
+            }
+        } else if (value instanceof byte[]){
+            prog.bindBlob(index, (byte[]) value);
+        } else {
+            prog.bindString(index, value.toString());
+        }
+    }
+
+    /**
+     * Bind the given selection with the given selection arguments.
+     * <p>
+     * Internally assumes that '?' is only ever used for arguments, and doesn't
+     * appear as a literal or escaped value.
+     * <p>
+     * This method is typically useful for trusted code that needs to cook up a
+     * fully-bound selection.
+     *
+     * @hide
+     */
+    public static @Nullable String bindSelection(@Nullable String selection,
+            @Nullable Object... selectionArgs) {
+        if (selection == null) return null;
+        // If no arguments provided, so we can't bind anything
+        if (ArrayUtils.isEmpty(selectionArgs)) return selection;
+        // If no bindings requested, so we can shortcut
+        if (selection.indexOf('?') == -1) return selection;
+
+        // Track the chars immediately before and after each bind request, to
+        // decide if it needs additional whitespace added
+        char before = ' ';
+        char after = ' ';
+
+        int argIndex = 0;
+        final int len = selection.length();
+        final StringBuilder res = new StringBuilder(len);
+        for (int i = 0; i < len; ) {
+            char c = selection.charAt(i++);
+            if (c == '?') {
+                // Assume this bind request is guarded until we find a specific
+                // trailing character below
+                after = ' ';
+
+                // Sniff forward to see if the selection is requesting a
+                // specific argument index
+                int start = i;
+                for (; i < len; i++) {
+                    c = selection.charAt(i);
+                    if (c < '0' || c > '9') {
+                        after = c;
+                        break;
+                    }
+                }
+                if (start != i) {
+                    argIndex = Integer.parseInt(selection.substring(start, i)) - 1;
+                }
+
+                // Manually bind the argument into the selection, adding
+                // whitespace when needed for clarity
+                final Object arg = selectionArgs[argIndex++];
+                if (before != ' ' && before != '=') res.append(' ');
+                switch (DatabaseUtils.getTypeOfObject(arg)) {
+                    case Cursor.FIELD_TYPE_NULL:
+                        res.append("NULL");
+                        break;
+                    case Cursor.FIELD_TYPE_INTEGER:
+                        res.append(((Number) arg).longValue());
+                        break;
+                    case Cursor.FIELD_TYPE_FLOAT:
+                        res.append(((Number) arg).doubleValue());
+                        break;
+                    case Cursor.FIELD_TYPE_BLOB:
+                        throw new IllegalArgumentException("Blobs not supported");
+                    case Cursor.FIELD_TYPE_STRING:
+                    default:
+                        if (arg instanceof Boolean) {
+                            // Provide compatibility with legacy applications which may pass
+                            // Boolean values in bind args.
+                            res.append(((Boolean) arg).booleanValue() ? 1 : 0);
+                        } else {
+                            res.append('\'');
+                            res.append(arg.toString());
+                            res.append('\'');
+                        }
+                        break;
+                }
+                if (after != ' ') res.append(' ');
+            } else {
+                res.append(c);
+                before = c;
+            }
+        }
+        return res.toString();
+    }
+
+    /**
+     * Make a deep copy of the given argument list, ensuring that the returned
+     * value is completely isolated from any changes to the original arguments.
+     *
+     * @hide
+     */
+    public static @Nullable Object[] deepCopyOf(@Nullable Object[] args) {
+        if (args == null) return null;
+
+        final Object[] res = new Object[args.length];
+        for (int i = 0; i < args.length; i++) {
+            final Object arg = args[i];
+
+            if ((arg == null) || (arg instanceof Number) || (arg instanceof String)) {
+                // When the argument is immutable, we can copy by reference
+                res[i] = arg;
+            } else if (arg instanceof byte[]) {
+                // Need to deep copy blobs
+                final byte[] castArg = (byte[]) arg;
+                res[i] = Arrays.copyOf(castArg, castArg.length);
+            } else {
+                // Convert everything else to string, making it immutable
+                res[i] = String.valueOf(arg);
+            }
+        }
+        return res;
+    }
+
+    /**
+     * Returns data type of the given object's value.
+     *<p>
+     * Returned values are
+     * <ul>
+     *   <li>{@link Cursor#FIELD_TYPE_NULL}</li>
+     *   <li>{@link Cursor#FIELD_TYPE_INTEGER}</li>
+     *   <li>{@link Cursor#FIELD_TYPE_FLOAT}</li>
+     *   <li>{@link Cursor#FIELD_TYPE_STRING}</li>
+     *   <li>{@link Cursor#FIELD_TYPE_BLOB}</li>
+     *</ul>
+     *</p>
+     *
+     * @param obj the object whose value type is to be returned
+     * @return object value type
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public static int getTypeOfObject(Object obj) {
+        if (obj == null) {
+            return Cursor.FIELD_TYPE_NULL;
+        } else if (obj instanceof byte[]) {
+            return Cursor.FIELD_TYPE_BLOB;
+        } else if (obj instanceof Float || obj instanceof Double) {
+            return Cursor.FIELD_TYPE_FLOAT;
+        } else if (obj instanceof Long || obj instanceof Integer
+                || obj instanceof Short || obj instanceof Byte) {
+            return Cursor.FIELD_TYPE_INTEGER;
+        } else {
+            return Cursor.FIELD_TYPE_STRING;
+        }
+    }
+
+    /**
+     * Fills the specified cursor window by iterating over the contents of the cursor.
+     * The window is filled until the cursor is exhausted or the window runs out
+     * of space.
+     *
+     * The original position of the cursor is left unchanged by this operation.
+     *
+     * @param cursor The cursor that contains the data to put in the window.
+     * @param position The start position for filling the window.
+     * @param window The window to fill.
+     * @hide
+     */
+    public static void cursorFillWindow(final Cursor cursor,
+            int position, final CursorWindow window) {
+        if (position < 0 || position >= cursor.getCount()) {
+            return;
+        }
+        final int oldPos = cursor.getPosition();
+        final int numColumns = cursor.getColumnCount();
+        window.clear();
+        window.setStartPosition(position);
+        window.setNumColumns(numColumns);
+        if (cursor.moveToPosition(position)) {
+            rowloop: do {
+                if (!window.allocRow()) {
+                    break;
+                }
+                for (int i = 0; i < numColumns; i++) {
+                    final int type = cursor.getType(i);
+                    final boolean success;
+                    switch (type) {
+                        case Cursor.FIELD_TYPE_NULL:
+                            success = window.putNull(position, i);
+                            break;
+
+                        case Cursor.FIELD_TYPE_INTEGER:
+                            success = window.putLong(cursor.getLong(i), position, i);
+                            break;
+
+                        case Cursor.FIELD_TYPE_FLOAT:
+                            success = window.putDouble(cursor.getDouble(i), position, i);
+                            break;
+
+                        case Cursor.FIELD_TYPE_BLOB: {
+                            final byte[] value = cursor.getBlob(i);
+                            success = value != null ? window.putBlob(value, position, i)
+                                    : window.putNull(position, i);
+                            break;
+                        }
+
+                        default: // assume value is convertible to String
+                        case Cursor.FIELD_TYPE_STRING: {
+                            final String value = cursor.getString(i);
+                            success = value != null ? window.putString(value, position, i)
+                                    : window.putNull(position, i);
+                            break;
+                        }
+                    }
+                    if (!success) {
+                        window.freeLastRow();
+                        break rowloop;
+                    }
+                }
+                position += 1;
+            } while (cursor.moveToNext());
+        }
+        cursor.moveToPosition(oldPos);
+    }
+
+    /**
+     * Appends an SQL string to the given StringBuilder, including the opening
+     * and closing single quotes. Any single quotes internal to sqlString will
+     * be escaped.
+     *
+     * This method is deprecated because we want to encourage everyone
+     * to use the "?" binding form.  However, when implementing a
+     * ContentProvider, one may want to add WHERE clauses that were
+     * not provided by the caller.  Since "?" is a positional form,
+     * using it in this case could break the caller because the
+     * indexes would be shifted to accomodate the ContentProvider's
+     * internal bindings.  In that case, it may be necessary to
+     * construct a WHERE clause manually.  This method is useful for
+     * those cases.
+     *
+     * @param sb the StringBuilder that the SQL string will be appended to
+     * @param sqlString the raw string to be appended, which may contain single
+     *                  quotes
+     */
+    public static void appendEscapedSQLString(StringBuilder sb, String sqlString) {
+        sb.append('\'');
+        if (sqlString.indexOf('\'') != -1) {
+            int length = sqlString.length();
+            for (int i = 0; i < length; i++) {
+                char c = sqlString.charAt(i);
+                if (c == '\'') {
+                    sb.append('\'');
+                }
+                sb.append(c);
+            }
+        } else
+            sb.append(sqlString);
+        sb.append('\'');
+    }
+
+    /**
+     * SQL-escape a string.
+     */
+    public static String sqlEscapeString(String value) {
+        StringBuilder escaper = new StringBuilder();
+
+        DatabaseUtils.appendEscapedSQLString(escaper, value);
+
+        return escaper.toString();
+    }
+
+    /**
+     * Appends an Object to an SQL string with the proper escaping, etc.
+     */
+    public static final void appendValueToSql(StringBuilder sql, Object value) {
+        if (value == null) {
+            sql.append("NULL");
+        } else if (value instanceof Boolean) {
+            Boolean bool = (Boolean)value;
+            if (bool) {
+                sql.append('1');
+            } else {
+                sql.append('0');
+            }
+        } else {
+            appendEscapedSQLString(sql, value.toString());
+        }
+    }
+
+    /**
+     * Concatenates two SQL WHERE clauses, handling empty or null values.
+     */
+    public static String concatenateWhere(String a, String b) {
+        if (TextUtils.isEmpty(a)) {
+            return b;
+        }
+        if (TextUtils.isEmpty(b)) {
+            return a;
+        }
+
+        return "(" + a + ") AND (" + b + ")";
+    }
+
+    /**
+     * return the collation key
+     * @param name
+     * @return the collation key
+     */
+    public static String getCollationKey(String name) {
+        byte [] arr = getCollationKeyInBytes(name);
+        try {
+            return new String(arr, 0, getKeyLen(arr), "ISO8859_1");
+        } catch (Exception ex) {
+            return "";
+        }
+    }
+
+    /**
+     * return the collation key in hex format
+     * @param name
+     * @return the collation key in hex format
+     */
+    public static String getHexCollationKey(String name) {
+        byte[] arr = getCollationKeyInBytes(name);
+        char[] keys = encodeHex(arr);
+        return new String(keys, 0, getKeyLen(arr) * 2);
+    }
+
+
+    /**
+     * Used building output as Hex
+     */
+    private static final char[] DIGITS = {
+            '0', '1', '2', '3', '4', '5', '6', '7',
+            '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
+    };
+
+    private static char[] encodeHex(byte[] input) {
+        int l = input.length;
+        char[] out = new char[l << 1];
+
+        // two characters form the hex value.
+        for (int i = 0, j = 0; i < l; i++) {
+            out[j++] = DIGITS[(0xF0 & input[i]) >>> 4 ];
+            out[j++] = DIGITS[ 0x0F & input[i] ];
+        }
+
+        return out;
+    }
+
+    private static int getKeyLen(byte[] arr) {
+        if (arr[arr.length - 1] != 0) {
+            return arr.length;
+        } else {
+            // remove zero "termination"
+            return arr.length-1;
+        }
+    }
+
+    private static byte[] getCollationKeyInBytes(String name) {
+        if (mColl == null) {
+            mColl = Collator.getInstance();
+            mColl.setStrength(Collator.PRIMARY);
+        }
+        return mColl.getCollationKey(name).toByteArray();
+    }
+
+    private static Collator mColl = null;
+    /**
+     * Prints the contents of a Cursor to System.out. The position is restored
+     * after printing.
+     *
+     * @param cursor the cursor to print
+     */
+    public static void dumpCursor(Cursor cursor) {
+        dumpCursor(cursor, System.out);
+    }
+
+    /**
+     * Prints the contents of a Cursor to a PrintSteam. The position is restored
+     * after printing.
+     *
+     * @param cursor the cursor to print
+     * @param stream the stream to print to
+     */
+    public static void dumpCursor(Cursor cursor, PrintStream stream) {
+        stream.println(">>>>> Dumping cursor " + cursor);
+        if (cursor != null) {
+            int startPos = cursor.getPosition();
+
+            cursor.moveToPosition(-1);
+            while (cursor.moveToNext()) {
+                dumpCurrentRow(cursor, stream);
+            }
+            cursor.moveToPosition(startPos);
+        }
+        stream.println("<<<<<");
+    }
+
+    /**
+     * Prints the contents of a Cursor to a StringBuilder. The position
+     * is restored after printing.
+     *
+     * @param cursor the cursor to print
+     * @param sb the StringBuilder to print to
+     */
+    public static void dumpCursor(Cursor cursor, StringBuilder sb) {
+        sb.append(">>>>> Dumping cursor " + cursor + "\n");
+        if (cursor != null) {
+            int startPos = cursor.getPosition();
+
+            cursor.moveToPosition(-1);
+            while (cursor.moveToNext()) {
+                dumpCurrentRow(cursor, sb);
+            }
+            cursor.moveToPosition(startPos);
+        }
+        sb.append("<<<<<\n");
+    }
+
+    /**
+     * Prints the contents of a Cursor to a String. The position is restored
+     * after printing.
+     *
+     * @param cursor the cursor to print
+     * @return a String that contains the dumped cursor
+     */
+    public static String dumpCursorToString(Cursor cursor) {
+        StringBuilder sb = new StringBuilder();
+        dumpCursor(cursor, sb);
+        return sb.toString();
+    }
+
+    /**
+     * Prints the contents of a Cursor's current row to System.out.
+     *
+     * @param cursor the cursor to print from
+     */
+    public static void dumpCurrentRow(Cursor cursor) {
+        dumpCurrentRow(cursor, System.out);
+    }
+
+    /**
+     * Prints the contents of a Cursor's current row to a PrintSteam.
+     *
+     * @param cursor the cursor to print
+     * @param stream the stream to print to
+     */
+    public static void dumpCurrentRow(Cursor cursor, PrintStream stream) {
+        String[] cols = cursor.getColumnNames();
+        stream.println("" + cursor.getPosition() + " {");
+        int length = cols.length;
+        for (int i = 0; i< length; i++) {
+            String value;
+            try {
+                value = cursor.getString(i);
+            } catch (SQLiteException e) {
+                // assume that if the getString threw this exception then the column is not
+                // representable by a string, e.g. it is a BLOB.
+                value = "<unprintable>";
+            }
+            stream.println("   " + cols[i] + '=' + value);
+        }
+        stream.println("}");
+    }
+
+    /**
+     * Prints the contents of a Cursor's current row to a StringBuilder.
+     *
+     * @param cursor the cursor to print
+     * @param sb the StringBuilder to print to
+     */
+    public static void dumpCurrentRow(Cursor cursor, StringBuilder sb) {
+        String[] cols = cursor.getColumnNames();
+        sb.append("" + cursor.getPosition() + " {\n");
+        int length = cols.length;
+        for (int i = 0; i < length; i++) {
+            String value;
+            try {
+                value = cursor.getString(i);
+            } catch (SQLiteException e) {
+                // assume that if the getString threw this exception then the column is not
+                // representable by a string, e.g. it is a BLOB.
+                value = "<unprintable>";
+            }
+            sb.append("   " + cols[i] + '=' + value + "\n");
+        }
+        sb.append("}\n");
+    }
+
+    /**
+     * Dump the contents of a Cursor's current row to a String.
+     *
+     * @param cursor the cursor to print
+     * @return a String that contains the dumped cursor row
+     */
+    public static String dumpCurrentRowToString(Cursor cursor) {
+        StringBuilder sb = new StringBuilder();
+        dumpCurrentRow(cursor, sb);
+        return sb.toString();
+    }
+
+    /**
+     * Reads a String out of a field in a Cursor and writes it to a Map.
+     *
+     * @param cursor The cursor to read from
+     * @param field The TEXT field to read
+     * @param values The {@link ContentValues} to put the value into, with the field as the key
+     */
+    public static void cursorStringToContentValues(Cursor cursor, String field,
+            ContentValues values) {
+        cursorStringToContentValues(cursor, field, values, field);
+    }
+
+    /**
+     * Reads a String out of a field in a Cursor and writes it to an InsertHelper.
+     *
+     * @param cursor The cursor to read from
+     * @param field The TEXT field to read
+     * @param inserter The InsertHelper to bind into
+     * @param index the index of the bind entry in the InsertHelper
+     */
+    public static void cursorStringToInsertHelper(Cursor cursor, String field,
+            InsertHelper inserter, int index) {
+        inserter.bind(index, cursor.getString(cursor.getColumnIndexOrThrow(field)));
+    }
+
+    /**
+     * Reads a String out of a field in a Cursor and writes it to a Map.
+     *
+     * @param cursor The cursor to read from
+     * @param field The TEXT field to read
+     * @param values The {@link ContentValues} to put the value into, with the field as the key
+     * @param key The key to store the value with in the map
+     */
+    public static void cursorStringToContentValues(Cursor cursor, String field,
+            ContentValues values, String key) {
+        values.put(key, cursor.getString(cursor.getColumnIndexOrThrow(field)));
+    }
+
+    /**
+     * Reads an Integer out of a field in a Cursor and writes it to a Map.
+     *
+     * @param cursor The cursor to read from
+     * @param field The INTEGER field to read
+     * @param values The {@link ContentValues} to put the value into, with the field as the key
+     */
+    public static void cursorIntToContentValues(Cursor cursor, String field, ContentValues values) {
+        cursorIntToContentValues(cursor, field, values, field);
+    }
+
+    /**
+     * Reads a Integer out of a field in a Cursor and writes it to a Map.
+     *
+     * @param cursor The cursor to read from
+     * @param field The INTEGER field to read
+     * @param values The {@link ContentValues} to put the value into, with the field as the key
+     * @param key The key to store the value with in the map
+     */
+    public static void cursorIntToContentValues(Cursor cursor, String field, ContentValues values,
+            String key) {
+        int colIndex = cursor.getColumnIndex(field);
+        if (!cursor.isNull(colIndex)) {
+            values.put(key, cursor.getInt(colIndex));
+        } else {
+            values.put(key, (Integer) null);
+        }
+    }
+
+    /**
+     * Reads a Long out of a field in a Cursor and writes it to a Map.
+     *
+     * @param cursor The cursor to read from
+     * @param field The INTEGER field to read
+     * @param values The {@link ContentValues} to put the value into, with the field as the key
+     */
+    public static void cursorLongToContentValues(Cursor cursor, String field, ContentValues values)
+    {
+        cursorLongToContentValues(cursor, field, values, field);
+    }
+
+    /**
+     * Reads a Long out of a field in a Cursor and writes it to a Map.
+     *
+     * @param cursor The cursor to read from
+     * @param field The INTEGER field to read
+     * @param values The {@link ContentValues} to put the value into
+     * @param key The key to store the value with in the map
+     */
+    public static void cursorLongToContentValues(Cursor cursor, String field, ContentValues values,
+            String key) {
+        int colIndex = cursor.getColumnIndex(field);
+        if (!cursor.isNull(colIndex)) {
+            Long value = Long.valueOf(cursor.getLong(colIndex));
+            values.put(key, value);
+        } else {
+            values.put(key, (Long) null);
+        }
+    }
+
+    /**
+     * Reads a Double out of a field in a Cursor and writes it to a Map.
+     *
+     * @param cursor The cursor to read from
+     * @param field The REAL field to read
+     * @param values The {@link ContentValues} to put the value into
+     */
+    public static void cursorDoubleToCursorValues(Cursor cursor, String field, ContentValues values)
+    {
+        cursorDoubleToContentValues(cursor, field, values, field);
+    }
+
+    /**
+     * Reads a Double out of a field in a Cursor and writes it to a Map.
+     *
+     * @param cursor The cursor to read from
+     * @param field The REAL field to read
+     * @param values The {@link ContentValues} to put the value into
+     * @param key The key to store the value with in the map
+     */
+    public static void cursorDoubleToContentValues(Cursor cursor, String field,
+            ContentValues values, String key) {
+        int colIndex = cursor.getColumnIndex(field);
+        if (!cursor.isNull(colIndex)) {
+            values.put(key, cursor.getDouble(colIndex));
+        } else {
+            values.put(key, (Double) null);
+        }
+    }
+
+    /**
+     * Read the entire contents of a cursor row and store them in a ContentValues.
+     *
+     * @param cursor the cursor to read from.
+     * @param values the {@link ContentValues} to put the row into.
+     */
+    public static void cursorRowToContentValues(Cursor cursor, ContentValues values) {
+        String[] columns = cursor.getColumnNames();
+        int length = columns.length;
+        for (int i = 0; i < length; i++) {
+            if (cursor.getType(i) == Cursor.FIELD_TYPE_BLOB) {
+                values.put(columns[i], cursor.getBlob(i));
+            } else {
+                values.put(columns[i], cursor.getString(i));
+            }
+        }
+    }
+
+    /**
+     * Picks a start position for {@link Cursor#fillWindow} such that the
+     * window will contain the requested row and a useful range of rows
+     * around it.
+     *
+     * When the data set is too large to fit in a cursor window, seeking the
+     * cursor can become a very expensive operation since we have to run the
+     * query again when we move outside the bounds of the current window.
+     *
+     * We try to choose a start position for the cursor window such that
+     * 1/3 of the window's capacity is used to hold rows before the requested
+     * position and 2/3 of the window's capacity is used to hold rows after the
+     * requested position.
+     *
+     * @param cursorPosition The row index of the row we want to get.
+     * @param cursorWindowCapacity The estimated number of rows that can fit in
+     * a cursor window, or 0 if unknown.
+     * @return The recommended start position, always less than or equal to
+     * the requested row.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public static int cursorPickFillWindowStartPosition(
+            int cursorPosition, int cursorWindowCapacity) {
+        return Math.max(cursorPosition - cursorWindowCapacity / 3, 0);
+    }
+
+    /**
+     * Query the table for the number of rows in the table.
+     * @param db the database the table is in
+     * @param table the name of the table to query
+     * @return the number of rows in the table
+     */
+    public static long queryNumEntries(SQLiteDatabase db, String table) {
+        return queryNumEntries(db, table, null, null);
+    }
+
+    /**
+     * Query the table for the number of rows in the table.
+     * @param db the database the table is in
+     * @param table the name of the table to query
+     * @param selection A filter declaring which rows to return,
+     *              formatted as an SQL WHERE clause (excluding the WHERE itself).
+     *              Passing null will count all rows for the given table
+     * @return the number of rows in the table filtered by the selection
+     */
+    public static long queryNumEntries(SQLiteDatabase db, String table, String selection) {
+        return queryNumEntries(db, table, selection, null);
+    }
+
+    /**
+     * Query the table for the number of rows in the table.
+     * @param db the database the table is in
+     * @param table the name of the table to query
+     * @param selection A filter declaring which rows to return,
+     *              formatted as an SQL WHERE clause (excluding the WHERE itself).
+     *              Passing null will count 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.
+     * @return the number of rows in the table filtered by the selection
+     */
+    public static long queryNumEntries(SQLiteDatabase db, String table, String selection,
+            String[] selectionArgs) {
+        String s = (!TextUtils.isEmpty(selection)) ? " where " + selection : "";
+        return longForQuery(db, "select count(*) from " + table + s,
+                    selectionArgs);
+    }
+
+    /**
+     * Query the table to check whether a table is empty or not
+     * @param db the database the table is in
+     * @param table the name of the table to query
+     * @return True if the table is empty
+     * @hide
+     */
+    public static boolean queryIsEmpty(SQLiteDatabase db, String table) {
+        long isEmpty = longForQuery(db, "select exists(select 1 from " + table + ")", null);
+        return isEmpty == 0;
+    }
+
+    /**
+     * Utility method to run the query on the db and return the value in the
+     * first column of the first row.
+     */
+    public static long longForQuery(SQLiteDatabase db, String query, String[] selectionArgs) {
+        SQLiteStatement prog = db.compileStatement(query);
+        try {
+            return longForQuery(prog, selectionArgs);
+        } finally {
+            prog.close();
+        }
+    }
+
+    /**
+     * Utility method to run the pre-compiled query and return the value in the
+     * first column of the first row.
+     */
+    public static long longForQuery(SQLiteStatement prog, String[] selectionArgs) {
+        prog.bindAllArgsAsStrings(selectionArgs);
+        return prog.simpleQueryForLong();
+    }
+
+    /**
+     * Utility method to run the query on the db and return the value in the
+     * first column of the first row.
+     */
+    public static String stringForQuery(SQLiteDatabase db, String query, String[] selectionArgs) {
+        SQLiteStatement prog = db.compileStatement(query);
+        try {
+            return stringForQuery(prog, selectionArgs);
+        } finally {
+            prog.close();
+        }
+    }
+
+    /**
+     * Utility method to run the pre-compiled query and return the value in the
+     * first column of the first row.
+     */
+    public static String stringForQuery(SQLiteStatement prog, String[] selectionArgs) {
+        prog.bindAllArgsAsStrings(selectionArgs);
+        return prog.simpleQueryForString();
+    }
+
+    /**
+     * Utility method to run the query on the db and return the blob value in the
+     * first column of the first row.
+     *
+     * @return A read-only file descriptor for a copy of the blob value.
+     */
+    public static ParcelFileDescriptor blobFileDescriptorForQuery(SQLiteDatabase db,
+            String query, String[] selectionArgs) {
+        SQLiteStatement prog = db.compileStatement(query);
+        try {
+            return blobFileDescriptorForQuery(prog, selectionArgs);
+        } finally {
+            prog.close();
+        }
+    }
+
+    /**
+     * Utility method to run the pre-compiled query and return the blob value in the
+     * first column of the first row.
+     *
+     * @return A read-only file descriptor for a copy of the blob value.
+     */
+    public static ParcelFileDescriptor blobFileDescriptorForQuery(SQLiteStatement prog,
+            String[] selectionArgs) {
+        prog.bindAllArgsAsStrings(selectionArgs);
+        return prog.simpleQueryForBlobFileDescriptor();
+    }
+
+    /**
+     * Reads a String out of a column in a Cursor and writes it to a ContentValues.
+     * Adds nothing to the ContentValues if the column isn't present or if its value is null.
+     *
+     * @param cursor The cursor to read from
+     * @param column The column to read
+     * @param values The {@link ContentValues} to put the value into
+     */
+    public static void cursorStringToContentValuesIfPresent(Cursor cursor, ContentValues values,
+            String column) {
+        final int index = cursor.getColumnIndex(column);
+        if (index != -1 && !cursor.isNull(index)) {
+            values.put(column, cursor.getString(index));
+        }
+    }
+
+    /**
+     * Reads a Long out of a column in a Cursor and writes it to a ContentValues.
+     * Adds nothing to the ContentValues if the column isn't present or if its value is null.
+     *
+     * @param cursor The cursor to read from
+     * @param column The column to read
+     * @param values The {@link ContentValues} to put the value into
+     */
+    public static void cursorLongToContentValuesIfPresent(Cursor cursor, ContentValues values,
+            String column) {
+        final int index = cursor.getColumnIndex(column);
+        if (index != -1 && !cursor.isNull(index)) {
+            values.put(column, cursor.getLong(index));
+        }
+    }
+
+    /**
+     * Reads a Short out of a column in a Cursor and writes it to a ContentValues.
+     * Adds nothing to the ContentValues if the column isn't present or if its value is null.
+     *
+     * @param cursor The cursor to read from
+     * @param column The column to read
+     * @param values The {@link ContentValues} to put the value into
+     */
+    public static void cursorShortToContentValuesIfPresent(Cursor cursor, ContentValues values,
+            String column) {
+        final int index = cursor.getColumnIndex(column);
+        if (index != -1 && !cursor.isNull(index)) {
+            values.put(column, cursor.getShort(index));
+        }
+    }
+
+    /**
+     * Reads a Integer out of a column in a Cursor and writes it to a ContentValues.
+     * Adds nothing to the ContentValues if the column isn't present or if its value is null.
+     *
+     * @param cursor The cursor to read from
+     * @param column The column to read
+     * @param values The {@link ContentValues} to put the value into
+     */
+    public static void cursorIntToContentValuesIfPresent(Cursor cursor, ContentValues values,
+            String column) {
+        final int index = cursor.getColumnIndex(column);
+        if (index != -1 && !cursor.isNull(index)) {
+            values.put(column, cursor.getInt(index));
+        }
+    }
+
+    /**
+     * Reads a Float out of a column in a Cursor and writes it to a ContentValues.
+     * Adds nothing to the ContentValues if the column isn't present or if its value is null.
+     *
+     * @param cursor The cursor to read from
+     * @param column The column to read
+     * @param values The {@link ContentValues} to put the value into
+     */
+    public static void cursorFloatToContentValuesIfPresent(Cursor cursor, ContentValues values,
+            String column) {
+        final int index = cursor.getColumnIndex(column);
+        if (index != -1 && !cursor.isNull(index)) {
+            values.put(column, cursor.getFloat(index));
+        }
+    }
+
+    /**
+     * Reads a Double out of a column in a Cursor and writes it to a ContentValues.
+     * Adds nothing to the ContentValues if the column isn't present or if its value is null.
+     *
+     * @param cursor The cursor to read from
+     * @param column The column to read
+     * @param values The {@link ContentValues} to put the value into
+     */
+    public static void cursorDoubleToContentValuesIfPresent(Cursor cursor, ContentValues values,
+            String column) {
+        final int index = cursor.getColumnIndex(column);
+        if (index != -1 && !cursor.isNull(index)) {
+            values.put(column, cursor.getDouble(index));
+        }
+    }
+
+    /**
+     * This class allows users to do multiple inserts into a table using
+     * the same statement.
+     * <p>
+     * This class is not thread-safe.
+     * </p>
+     *
+     * @deprecated Use {@link SQLiteStatement} instead.
+     */
+    @Deprecated
+    public static class InsertHelper {
+        private final SQLiteDatabase mDb;
+        private final String mTableName;
+        private HashMap<String, Integer> mColumns;
+        private String mInsertSQL = null;
+        private SQLiteStatement mInsertStatement = null;
+        private SQLiteStatement mReplaceStatement = null;
+        private SQLiteStatement mPreparedStatement = null;
+
+        /**
+         * {@hide}
+         *
+         * These are the columns returned by sqlite's "PRAGMA
+         * table_info(...)" command that we depend on.
+         */
+        public static final int TABLE_INFO_PRAGMA_COLUMNNAME_INDEX = 1;
+
+        /**
+         * This field was accidentally exposed in earlier versions of the platform
+         * so we can hide it but we can't remove it.
+         *
+         * @hide
+         */
+        public static final int TABLE_INFO_PRAGMA_DEFAULT_INDEX = 4;
+
+        /**
+         * @param db the SQLiteDatabase to insert into
+         * @param tableName the name of the table to insert into
+         */
+        public InsertHelper(SQLiteDatabase db, String tableName) {
+            mDb = db;
+            mTableName = tableName;
+        }
+
+        private void buildSQL() throws SQLException {
+            StringBuilder sb = new StringBuilder(128);
+            sb.append("INSERT INTO ");
+            sb.append(mTableName);
+            sb.append(" (");
+
+            StringBuilder sbv = new StringBuilder(128);
+            sbv.append("VALUES (");
+
+            int i = 1;
+            Cursor cur = null;
+            try {
+                cur = mDb.rawQuery("PRAGMA table_info(" + mTableName + ")", null);
+                mColumns = new HashMap<String, Integer>(cur.getCount());
+                while (cur.moveToNext()) {
+                    String columnName = cur.getString(TABLE_INFO_PRAGMA_COLUMNNAME_INDEX);
+                    String defaultValue = cur.getString(TABLE_INFO_PRAGMA_DEFAULT_INDEX);
+
+                    mColumns.put(columnName, i);
+                    sb.append("'");
+                    sb.append(columnName);
+                    sb.append("'");
+
+                    if (defaultValue == null) {
+                        sbv.append("?");
+                    } else {
+                        sbv.append("COALESCE(?, ");
+                        sbv.append(defaultValue);
+                        sbv.append(")");
+                    }
+
+                    sb.append(i == cur.getCount() ? ") " : ", ");
+                    sbv.append(i == cur.getCount() ? ");" : ", ");
+                    ++i;
+                }
+            } finally {
+                if (cur != null) cur.close();
+            }
+
+            sb.append(sbv);
+
+            mInsertSQL = sb.toString();
+            if (DEBUG) Log.v(TAG, "insert statement is " + mInsertSQL);
+        }
+
+        private SQLiteStatement getStatement(boolean allowReplace) throws SQLException {
+            if (allowReplace) {
+                if (mReplaceStatement == null) {
+                    if (mInsertSQL == null) buildSQL();
+                    // chop "INSERT" off the front and prepend "INSERT OR REPLACE" instead.
+                    String replaceSQL = "INSERT OR REPLACE" + mInsertSQL.substring(6);
+                    mReplaceStatement = mDb.compileStatement(replaceSQL);
+                }
+                return mReplaceStatement;
+            } else {
+                if (mInsertStatement == null) {
+                    if (mInsertSQL == null) buildSQL();
+                    mInsertStatement = mDb.compileStatement(mInsertSQL);
+                }
+                return mInsertStatement;
+            }
+        }
+
+        /**
+         * Performs an insert, adding a new row with the given values.
+         *
+         * @param values the set of values with which  to populate the
+         * new row
+         * @param allowReplace if true, the statement does "INSERT OR
+         *   REPLACE" instead of "INSERT", silently deleting any
+         *   previously existing rows that would cause a conflict
+         *
+         * @return the row ID of the newly inserted row, or -1 if an
+         * error occurred
+         */
+        private long insertInternal(ContentValues values, boolean allowReplace) {
+            // Start a transaction even though we don't really need one.
+            // This is to help maintain compatibility with applications that
+            // access InsertHelper from multiple threads even though they never should have.
+            // The original code used to lock the InsertHelper itself which was prone
+            // to deadlocks.  Starting a transaction achieves the same mutual exclusion
+            // effect as grabbing a lock but without the potential for deadlocks.
+            mDb.beginTransactionNonExclusive();
+            try {
+                SQLiteStatement stmt = getStatement(allowReplace);
+                stmt.clearBindings();
+                if (DEBUG) Log.v(TAG, "--- inserting in table " + mTableName);
+                for (Map.Entry<String, Object> e: values.valueSet()) {
+                    final String key = e.getKey();
+                    int i = getColumnIndex(key);
+                    DatabaseUtils.bindObjectToProgram(stmt, i, e.getValue());
+                    if (DEBUG) {
+                        Log.v(TAG, "binding " + e.getValue() + " to column " +
+                              i + " (" + key + ")");
+                    }
+                }
+                long result = stmt.executeInsert();
+                mDb.setTransactionSuccessful();
+                return result;
+            } catch (SQLException e) {
+                Log.e(TAG, "Error inserting " + values + " into table  " + mTableName, e);
+                return -1;
+            } finally {
+                mDb.endTransaction();
+            }
+        }
+
+        /**
+         * Returns the index of the specified column. This is index is suitagble for use
+         * in calls to bind().
+         * @param key the column name
+         * @return the index of the column
+         */
+        public int getColumnIndex(String key) {
+            getStatement(false);
+            final Integer index = mColumns.get(key);
+            if (index == null) {
+                throw new IllegalArgumentException("column '" + key + "' is invalid");
+            }
+            return index;
+        }
+
+        /**
+         * Bind the value to an index. A prepareForInsert() or prepareForReplace()
+         * without a matching execute() must have already have been called.
+         * @param index the index of the slot to which to bind
+         * @param value the value to bind
+         */
+        public void bind(int index, double value) {
+            mPreparedStatement.bindDouble(index, value);
+        }
+
+        /**
+         * Bind the value to an index. A prepareForInsert() or prepareForReplace()
+         * without a matching execute() must have already have been called.
+         * @param index the index of the slot to which to bind
+         * @param value the value to bind
+         */
+        public void bind(int index, float value) {
+            mPreparedStatement.bindDouble(index, value);
+        }
+
+        /**
+         * Bind the value to an index. A prepareForInsert() or prepareForReplace()
+         * without a matching execute() must have already have been called.
+         * @param index the index of the slot to which to bind
+         * @param value the value to bind
+         */
+        public void bind(int index, long value) {
+            mPreparedStatement.bindLong(index, value);
+        }
+
+        /**
+         * Bind the value to an index. A prepareForInsert() or prepareForReplace()
+         * without a matching execute() must have already have been called.
+         * @param index the index of the slot to which to bind
+         * @param value the value to bind
+         */
+        public void bind(int index, int value) {
+            mPreparedStatement.bindLong(index, value);
+        }
+
+        /**
+         * Bind the value to an index. A prepareForInsert() or prepareForReplace()
+         * without a matching execute() must have already have been called.
+         * @param index the index of the slot to which to bind
+         * @param value the value to bind
+         */
+        public void bind(int index, boolean value) {
+            mPreparedStatement.bindLong(index, value ? 1 : 0);
+        }
+
+        /**
+         * Bind null to an index. A prepareForInsert() or prepareForReplace()
+         * without a matching execute() must have already have been called.
+         * @param index the index of the slot to which to bind
+         */
+        public void bindNull(int index) {
+            mPreparedStatement.bindNull(index);
+        }
+
+        /**
+         * Bind the value to an index. A prepareForInsert() or prepareForReplace()
+         * without a matching execute() must have already have been called.
+         * @param index the index of the slot to which to bind
+         * @param value the value to bind
+         */
+        public void bind(int index, byte[] value) {
+            if (value == null) {
+                mPreparedStatement.bindNull(index);
+            } else {
+                mPreparedStatement.bindBlob(index, value);
+            }
+        }
+
+        /**
+         * Bind the value to an index. A prepareForInsert() or prepareForReplace()
+         * without a matching execute() must have already have been called.
+         * @param index the index of the slot to which to bind
+         * @param value the value to bind
+         */
+        public void bind(int index, String value) {
+            if (value == null) {
+                mPreparedStatement.bindNull(index);
+            } else {
+                mPreparedStatement.bindString(index, value);
+            }
+        }
+
+        /**
+         * Performs an insert, adding a new row with the given values.
+         * If the table contains conflicting rows, an error is
+         * returned.
+         *
+         * @param values the set of values with which to populate the
+         * new row
+         *
+         * @return the row ID of the newly inserted row, or -1 if an
+         * error occurred
+         */
+        public long insert(ContentValues values) {
+            return insertInternal(values, false);
+        }
+
+        /**
+         * Execute the previously prepared insert or replace using the bound values
+         * since the last call to prepareForInsert or prepareForReplace.
+         *
+         * <p>Note that calling bind() and then execute() is not thread-safe. The only thread-safe
+         * way to use this class is to call insert() or replace().
+         *
+         * @return the row ID of the newly inserted row, or -1 if an
+         * error occurred
+         */
+        public long execute() {
+            if (mPreparedStatement == null) {
+                throw new IllegalStateException("you must prepare this inserter before calling "
+                        + "execute");
+            }
+            try {
+                if (DEBUG) Log.v(TAG, "--- doing insert or replace in table " + mTableName);
+                return mPreparedStatement.executeInsert();
+            } catch (SQLException e) {
+                Log.e(TAG, "Error executing InsertHelper with table " + mTableName, e);
+                return -1;
+            } finally {
+                // you can only call this once per prepare
+                mPreparedStatement = null;
+            }
+        }
+
+        /**
+         * Prepare the InsertHelper for an insert. The pattern for this is:
+         * <ul>
+         * <li>prepareForInsert()
+         * <li>bind(index, value);
+         * <li>bind(index, value);
+         * <li>...
+         * <li>bind(index, value);
+         * <li>execute();
+         * </ul>
+         */
+        public void prepareForInsert() {
+            mPreparedStatement = getStatement(false);
+            mPreparedStatement.clearBindings();
+        }
+
+        /**
+         * Prepare the InsertHelper for a replace. The pattern for this is:
+         * <ul>
+         * <li>prepareForReplace()
+         * <li>bind(index, value);
+         * <li>bind(index, value);
+         * <li>...
+         * <li>bind(index, value);
+         * <li>execute();
+         * </ul>
+         */
+        public void prepareForReplace() {
+            mPreparedStatement = getStatement(true);
+            mPreparedStatement.clearBindings();
+        }
+
+        /**
+         * Performs an insert, adding a new row with the given values.
+         * If the table contains conflicting rows, they are deleted
+         * and replaced with the new row.
+         *
+         * @param values the set of values with which to populate the
+         * new row
+         *
+         * @return the row ID of the newly inserted row, or -1 if an
+         * error occurred
+         */
+        public long replace(ContentValues values) {
+            return insertInternal(values, true);
+        }
+
+        /**
+         * Close this object and release any resources associated with
+         * it.  The behavior of calling <code>insert()</code> after
+         * calling this method is undefined.
+         */
+        public void close() {
+            if (mInsertStatement != null) {
+                mInsertStatement.close();
+                mInsertStatement = null;
+            }
+            if (mReplaceStatement != null) {
+                mReplaceStatement.close();
+                mReplaceStatement = null;
+            }
+            mInsertSQL = null;
+            mColumns = null;
+        }
+    }
+
+    /**
+     * Creates a db and populates it with the sql statements in sqlStatements.
+     *
+     * @param context the context to use to create the db
+     * @param dbName the name of the db to create
+     * @param dbVersion the version to set on the db
+     * @param sqlStatements the statements to use to populate the db. This should be a single string
+     *   of the form returned by sqlite3's <tt>.dump</tt> command (statements separated by
+     *   semicolons)
+     */
+    static public void createDbFromSqlStatements(
+            Context context, String dbName, int dbVersion, String sqlStatements) {
+        SQLiteDatabase db = context.openOrCreateDatabase(dbName, 0, null);
+        // TODO: this is not quite safe since it assumes that all semicolons at the end of a line
+        // terminate statements. It is possible that a text field contains ;\n. We will have to fix
+        // this if that turns out to be a problem.
+        String[] statements = TextUtils.split(sqlStatements, ";\n");
+        for (String statement : statements) {
+            if (TextUtils.isEmpty(statement)) continue;
+            db.execSQL(statement);
+        }
+        db.setVersion(dbVersion);
+        db.close();
+    }
+
+    /**
+     * Returns one of the following which represent the type of the given SQL statement.
+     * <ol>
+     *   <li>{@link #STATEMENT_SELECT}</li>
+     *   <li>{@link #STATEMENT_UPDATE}</li>
+     *   <li>{@link #STATEMENT_ATTACH}</li>
+     *   <li>{@link #STATEMENT_BEGIN}</li>
+     *   <li>{@link #STATEMENT_COMMIT}</li>
+     *   <li>{@link #STATEMENT_ABORT}</li>
+     *   <li>{@link #STATEMENT_OTHER}</li>
+     * </ol>
+     * @param sql the SQL statement whose type is returned by this method
+     * @return one of the values listed above
+     */
+    public static int getSqlStatementType(String sql) {
+        sql = sql.trim();
+        if (sql.length() < 3) {
+            return STATEMENT_OTHER;
+        }
+        String prefixSql = sql.substring(0, 3).toUpperCase(Locale.ROOT);
+        if (prefixSql.equals("SEL")) {
+            return STATEMENT_SELECT;
+        } else if (prefixSql.equals("INS") ||
+                prefixSql.equals("UPD") ||
+                prefixSql.equals("REP") ||
+                prefixSql.equals("DEL")) {
+            return STATEMENT_UPDATE;
+        } else if (prefixSql.equals("ATT")) {
+            return STATEMENT_ATTACH;
+        } else if (prefixSql.equals("COM")) {
+            return STATEMENT_COMMIT;
+        } else if (prefixSql.equals("END")) {
+            return STATEMENT_COMMIT;
+        } else if (prefixSql.equals("ROL")) {
+            boolean isRollbackToSavepoint = sql.toUpperCase(Locale.ROOT).contains(" TO ");
+            if (isRollbackToSavepoint) {
+                Log.w(TAG, "Statement '" + sql
+                        + "' may not work on API levels 16-27, use ';" + sql + "' instead");
+                return STATEMENT_OTHER;
+            }
+            return STATEMENT_ABORT;
+        } else if (prefixSql.equals("BEG")) {
+            return STATEMENT_BEGIN;
+        } else if (prefixSql.equals("PRA")) {
+            return STATEMENT_PRAGMA;
+        } else if (prefixSql.equals("CRE") || prefixSql.equals("DRO") ||
+                prefixSql.equals("ALT")) {
+            return STATEMENT_DDL;
+        } else if (prefixSql.equals("ANA") || prefixSql.equals("DET")) {
+            return STATEMENT_UNPREPARED;
+        }
+        return STATEMENT_OTHER;
+    }
+
+    /**
+     * Appends one set of selection args to another. This is useful when adding a selection
+     * argument to a user provided set.
+     */
+    public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) {
+        if (originalValues == null || originalValues.length == 0) {
+            return newValues;
+        }
+        String[] result = new String[originalValues.length + newValues.length ];
+        System.arraycopy(originalValues, 0, result, 0, originalValues.length);
+        System.arraycopy(newValues, 0, result, originalValues.length, newValues.length);
+        return result;
+    }
+
+    /**
+     * Returns column index of "_id" column, or -1 if not found.
+     * @hide
+     */
+    public static int findRowIdColumnIndex(String[] columnNames) {
+        int length = columnNames.length;
+        for (int i = 0; i < length; i++) {
+            if (columnNames[i].equals("_id")) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Escape the given argument for use in a {@code LIKE} statement.
+     * @hide
+     */
+    public static String escapeForLike(@NonNull String arg) {
+        // Shamelessly borrowed from com.android.providers.media.util.DatabaseUtils
+        final StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < arg.length(); i++) {
+            final char c = arg.charAt(i);
+            switch (c) {
+                case '%': sb.append('\\');
+                    break;
+                case '_': sb.append('\\');
+                    break;
+            }
+            sb.append(c);
+        }
+        return sb.toString();
+    }
+}
diff --git a/android/database/DefaultDatabaseErrorHandler.java b/android/database/DefaultDatabaseErrorHandler.java
new file mode 100644
index 0000000..cf019e1
--- /dev/null
+++ b/android/database/DefaultDatabaseErrorHandler.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2010 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 android.database;
+
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Default class used to define the action to take when database corruption is reported
+ * by sqlite.
+ * <p>
+ * An application can specify an implementation of {@link DatabaseErrorHandler} on the
+ * following:
+ * <ul>
+ *   <li>{@link SQLiteDatabase#openOrCreateDatabase(String,
+ *      android.database.sqlite.SQLiteDatabase.CursorFactory, DatabaseErrorHandler)}</li>
+ *   <li>{@link SQLiteDatabase#openDatabase(String,
+ *      android.database.sqlite.SQLiteDatabase.CursorFactory, int, DatabaseErrorHandler)}</li>
+ * </ul>
+ * The specified {@link DatabaseErrorHandler} is used to handle database corruption errors, if they
+ * occur.
+ * <p>
+ * If null is specified for the DatabaseErrorHandler param in the above calls, this class is used
+ * as the default {@link DatabaseErrorHandler}.
+ */
+public final class DefaultDatabaseErrorHandler implements DatabaseErrorHandler {
+
+    private static final String TAG = "DefaultDatabaseErrorHandler";
+
+    /**
+     * defines the default method to be invoked when database corruption is detected.
+     * @param dbObj the {@link SQLiteDatabase} object representing the database on which corruption
+     * is detected.
+     */
+    public void onCorruption(SQLiteDatabase dbObj) {
+        Log.e(TAG, "Corruption reported by sqlite on database: " + dbObj.getPath());
+        SQLiteDatabase.wipeDetected(dbObj.getPath(), "corruption");
+
+        // is the corruption detected even before database could be 'opened'?
+        if (!dbObj.isOpen()) {
+            // database files are not even openable. delete this database file.
+            // NOTE if the database has attached databases, then any of them could be corrupt.
+            // and not deleting all of them could cause corrupted database file to remain and 
+            // make the application crash on database open operation. To avoid this problem,
+            // the application should provide its own {@link DatabaseErrorHandler} impl class
+            // to delete ALL files of the database (including the attached databases).
+            deleteDatabaseFile(dbObj.getPath());
+            return;
+        }
+
+        List<Pair<String, String>> attachedDbs = null;
+        try {
+            // Close the database, which will cause subsequent operations to fail.
+            // before that, get the attached database list first.
+            try {
+                attachedDbs = dbObj.getAttachedDbs();
+            } catch (SQLiteException e) {
+                /* ignore */
+            }
+            try {
+                dbObj.close();
+            } catch (SQLiteException e) {
+                /* ignore */
+            }
+        } finally {
+            // Delete all files of this corrupt database and/or attached databases
+            if (attachedDbs != null) {
+                for (Pair<String, String> p : attachedDbs) {
+                    deleteDatabaseFile(p.second);
+                }
+            } else {
+                // attachedDbs = null is possible when the database is so corrupt that even
+                // "PRAGMA database_list;" also fails. delete the main database file
+                deleteDatabaseFile(dbObj.getPath());
+            }
+        }
+    }
+
+    private void deleteDatabaseFile(String fileName) {
+        if (fileName.equalsIgnoreCase(":memory:") || fileName.trim().length() == 0) {
+            return;
+        }
+        Log.e(TAG, "deleting the database file: " + fileName);
+        try {
+            SQLiteDatabase.deleteDatabase(new File(fileName), /*removeCheckFile=*/ false);
+        } catch (Exception e) {
+            /* print warning and ignore exception */
+            Log.w(TAG, "delete failed: " + e.getMessage());
+        }
+    }
+}
diff --git a/android/database/IBulkCursor.java b/android/database/IBulkCursor.java
new file mode 100644
index 0000000..b551116
--- /dev/null
+++ b/android/database/IBulkCursor.java
@@ -0,0 +1,66 @@
+/*
+ * 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 android.database;
+
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.RemoteException;
+
+/**
+ * This interface provides a low-level way to pass bulk cursor data across
+ * both process and language boundaries. Application code should use the Cursor
+ * interface directly.
+ *
+ * {@hide}
+ */
+public interface IBulkCursor extends IInterface  {
+    /**
+     * Gets a cursor window that contains the specified position.
+     * The window will contain a range of rows around the specified position.
+     */
+    public CursorWindow getWindow(int position) throws RemoteException;
+
+    /**
+     * Notifies the cursor that the position has changed.
+     * Only called when {@link #getWantsAllOnMoveCalls()} returns true.
+     *
+     * @param position The new position
+     */
+    public void onMove(int position) throws RemoteException;
+
+    public void deactivate() throws RemoteException;
+
+    public void close() throws RemoteException;
+
+    public int requery(IContentObserver observer) throws RemoteException;
+
+    Bundle getExtras() throws RemoteException;
+
+    Bundle respond(Bundle extras) throws RemoteException;
+
+    /* IPC constants */
+    static final String descriptor = "android.content.IBulkCursor";
+
+    static final int GET_CURSOR_WINDOW_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION;
+    static final int DEACTIVATE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 1;
+    static final int REQUERY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 2;
+    static final int ON_MOVE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 3;
+    static final int GET_EXTRAS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 4;
+    static final int RESPOND_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 5;
+    static final int CLOSE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 6;
+}
diff --git a/android/database/MatrixCursor.java b/android/database/MatrixCursor.java
new file mode 100644
index 0000000..050a49a
--- /dev/null
+++ b/android/database/MatrixCursor.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2007 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 android.database;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+
+import java.util.ArrayList;
+
+/**
+ * A mutable cursor implementation backed by an array of {@code Object}s. Use
+ * {@link #newRow()} to add rows. Automatically expands internal capacity
+ * as needed.
+ */
+public class MatrixCursor extends AbstractCursor {
+
+    private final String[] columnNames;
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    private Object[] data;
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    private int rowCount = 0;
+    private final int columnCount;
+
+    /**
+     * Constructs a new cursor with the given initial capacity.
+     *
+     * @param columnNames names of the columns, the ordering of which
+     *  determines column ordering elsewhere in this cursor
+     * @param initialCapacity in rows
+     */
+    public MatrixCursor(String[] columnNames, int initialCapacity) {
+        this.columnNames = columnNames;
+        this.columnCount = columnNames.length;
+
+        if (initialCapacity < 1) {
+            initialCapacity = 1;
+        }
+
+        this.data = new Object[columnCount * initialCapacity];
+    }
+
+    /**
+     * Constructs a new cursor.
+     *
+     * @param columnNames names of the columns, the ordering of which
+     *  determines column ordering elsewhere in this cursor
+     */
+    public MatrixCursor(String[] columnNames) {
+        this(columnNames, 16);
+    }
+
+    /**
+     * Gets value at the given column for the current row.
+     */
+    @UnsupportedAppUsage
+    private Object get(int column) {
+        if (column < 0 || column >= columnCount) {
+            throw new CursorIndexOutOfBoundsException("Requested column: "
+                    + column + ", # of columns: " +  columnCount);
+        }
+        if (mPos < 0) {
+            throw new CursorIndexOutOfBoundsException("Before first row.");
+        }
+        if (mPos >= rowCount) {
+            throw new CursorIndexOutOfBoundsException("After last row.");
+        }
+        return data[mPos * columnCount + column];
+    }
+
+    /**
+     * Adds a new row to the end and returns a builder for that row. Not safe
+     * for concurrent use.
+     *
+     * @return builder which can be used to set the column values for the new
+     *  row
+     */
+    public RowBuilder newRow() {
+        final int row = rowCount++;
+        final int endIndex = rowCount * columnCount;
+        ensureCapacity(endIndex);
+        return new RowBuilder(row);
+    }
+
+    /**
+     * Adds a new row to the end with the given column values. Not safe
+     * for concurrent use.
+     *
+     * @throws IllegalArgumentException if {@code columnValues.length !=
+     *  columnNames.length}
+     * @param columnValues in the same order as the the column names specified
+     *  at cursor construction time
+     */
+    public void addRow(Object[] columnValues) {
+        if (columnValues.length != columnCount) {
+            throw new IllegalArgumentException("columnNames.length = "
+                    + columnCount + ", columnValues.length = "
+                    + columnValues.length);
+        }
+
+        int start = rowCount++ * columnCount;
+        ensureCapacity(start + columnCount);
+        System.arraycopy(columnValues, 0, data, start, columnCount);
+    }
+
+    /**
+     * Adds a new row to the end with the given column values. Not safe
+     * for concurrent use.
+     *
+     * @throws IllegalArgumentException if {@code columnValues.size() !=
+     *  columnNames.length}
+     * @param columnValues in the same order as the the column names specified
+     *  at cursor construction time
+     */
+    public void addRow(Iterable<?> columnValues) {
+        int start = rowCount * columnCount;
+        int end = start + columnCount;
+        ensureCapacity(end);
+
+        if (columnValues instanceof ArrayList<?>) {
+            addRow((ArrayList<?>) columnValues, start);
+            return;
+        }
+
+        int current = start;
+        Object[] localData = data;
+        for (Object columnValue : columnValues) {
+            if (current == end) {
+                // TODO: null out row?
+                throw new IllegalArgumentException(
+                        "columnValues.size() > columnNames.length");
+            }
+            localData[current++] = columnValue;
+        }
+
+        if (current != end) {
+            // TODO: null out row?
+            throw new IllegalArgumentException(
+                    "columnValues.size() < columnNames.length");
+        }
+
+        // Increase row count here in case we encounter an exception.
+        rowCount++;
+    }
+
+    /** Optimization for {@link ArrayList}. */
+    private void addRow(ArrayList<?> columnValues, int start) {
+        int size = columnValues.size();
+        if (size != columnCount) {
+            throw new IllegalArgumentException("columnNames.length = "
+                    + columnCount + ", columnValues.size() = " + size);
+        }
+
+        rowCount++;
+        Object[] localData = data;
+        for (int i = 0; i < size; i++) {
+            localData[start + i] = columnValues.get(i);
+        }
+    }
+
+    /** Ensures that this cursor has enough capacity. */
+    private void ensureCapacity(int size) {
+        if (size > data.length) {
+            Object[] oldData = this.data;
+            int newSize = data.length * 2;
+            if (newSize < size) {
+                newSize = size;
+            }
+            this.data = new Object[newSize];
+            System.arraycopy(oldData, 0, this.data, 0, oldData.length);
+        }
+    }
+
+    /**
+     * Builds a row of values using either of these approaches:
+     * <ul>
+     * <li>Values can be added with explicit column ordering using
+     * {@link #add(Object)}, which starts from the left-most column and adds one
+     * column value at a time. This follows the same ordering as the column
+     * names specified at cursor construction time.
+     * <li>Column and value pairs can be offered for possible inclusion using
+     * {@link #add(String, Object)}. If the cursor includes the given column,
+     * the value will be set for that column, otherwise the value is ignored.
+     * This approach is useful when matching data to a custom projection.
+     * </ul>
+     * Undefined values are left as {@code null}.
+     */
+    public class RowBuilder {
+        private final int row;
+        private final int endIndex;
+
+        private int index;
+
+        RowBuilder(int row) {
+            this.row = row;
+            this.index = row * columnCount;
+            this.endIndex = index + columnCount;
+        }
+
+        /**
+         * Sets the next column value in this row.
+         *
+         * @throws CursorIndexOutOfBoundsException if you try to add too many
+         *  values
+         * @return this builder to support chaining
+         */
+        public RowBuilder add(Object columnValue) {
+            if (index == endIndex) {
+                throw new CursorIndexOutOfBoundsException(
+                        "No more columns left.");
+            }
+
+            data[index++] = columnValue;
+            return this;
+        }
+
+        /**
+         * Offer value for possible inclusion if this cursor defines the given
+         * column. Columns not defined by the cursor are silently ignored.
+         *
+         * @return this builder to support chaining
+         */
+        public RowBuilder add(String columnName, Object value) {
+            for (int i = 0; i < columnNames.length; i++) {
+                if (columnName.equals(columnNames[i])) {
+                    data[(row * columnCount) + i] = value;
+                }
+            }
+            return this;
+        }
+
+        /** @hide */
+        public final RowBuilder add(int columnIndex, Object value) {
+            data[(row * columnCount) + columnIndex] = value;
+            return this;
+        }
+    }
+
+    // AbstractCursor implementation.
+
+    @Override
+    public int getCount() {
+        return rowCount;
+    }
+
+    @Override
+    public String[] getColumnNames() {
+        return columnNames;
+    }
+
+    @Override
+    public String getString(int column) {
+        Object value = get(column);
+        if (value == null) return null;
+        return value.toString();
+    }
+
+    @Override
+    public short getShort(int column) {
+        Object value = get(column);
+        if (value == null) return 0;
+        if (value instanceof Number) return ((Number) value).shortValue();
+        return Short.parseShort(value.toString());
+    }
+
+    @Override
+    public int getInt(int column) {
+        Object value = get(column);
+        if (value == null) return 0;
+        if (value instanceof Number) return ((Number) value).intValue();
+        return Integer.parseInt(value.toString());
+    }
+
+    @Override
+    public long getLong(int column) {
+        Object value = get(column);
+        if (value == null) return 0;
+        if (value instanceof Number) return ((Number) value).longValue();
+        return Long.parseLong(value.toString());
+    }
+
+    @Override
+    public float getFloat(int column) {
+        Object value = get(column);
+        if (value == null) return 0.0f;
+        if (value instanceof Number) return ((Number) value).floatValue();
+        return Float.parseFloat(value.toString());
+    }
+
+    @Override
+    public double getDouble(int column) {
+        Object value = get(column);
+        if (value == null) return 0.0d;
+        if (value instanceof Number) return ((Number) value).doubleValue();
+        return Double.parseDouble(value.toString());
+    }
+
+    @Override
+    public byte[] getBlob(int column) {
+        Object value = get(column);
+        return (byte[]) value;
+    }
+
+    @Override
+    public int getType(int column) {
+        return DatabaseUtils.getTypeOfObject(get(column));
+    }
+
+    @Override
+    public boolean isNull(int column) {
+        return get(column) == null;
+    }
+}
diff --git a/android/database/MergeCursor.java b/android/database/MergeCursor.java
new file mode 100644
index 0000000..272cfa2
--- /dev/null
+++ b/android/database/MergeCursor.java
@@ -0,0 +1,238 @@
+/*
+ * 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 android.database;
+
+/**
+ * A convenience class that lets you present an array of Cursors as a single linear Cursor.
+ * The schema of the cursors presented is entirely up to the creator of the MergeCursor, and
+ * may be different if that is desired. Calls to getColumns, getColumnIndex, etc will return the
+ * value for the row that the MergeCursor is currently pointing at.
+ */
+public class MergeCursor extends AbstractCursor
+{
+    private DataSetObserver mObserver = new DataSetObserver() {
+
+        @Override
+        public void onChanged() {
+            // Reset our position so the optimizations in move-related code
+            // don't screw us over
+            mPos = -1;
+        }
+
+        @Override
+        public void onInvalidated() {
+            mPos = -1;
+        }
+    };
+    
+    public MergeCursor(Cursor[] cursors)
+    {
+        mCursors = cursors;
+        mCursor = cursors[0];
+        
+        for (int i = 0; i < mCursors.length; i++) {
+            if (mCursors[i] == null) continue;
+            
+            mCursors[i].registerDataSetObserver(mObserver);
+        }
+    }
+    
+    @Override
+    public int getCount()
+    {
+        int count = 0;
+        int length = mCursors.length;
+        for (int i = 0 ; i < length ; i++) {
+            if (mCursors[i] != null) {
+                count += mCursors[i].getCount();
+            }
+        }
+        return count;
+    }
+
+    @Override
+    public boolean onMove(int oldPosition, int newPosition)
+    {
+        /* Find the right cursor */
+        mCursor = null;
+        int cursorStartPos = 0;
+        int length = mCursors.length;
+        for (int i = 0 ; i < length; i++) {
+            if (mCursors[i] == null) {
+                continue;
+            }
+            
+            if (newPosition < (cursorStartPos + mCursors[i].getCount())) {
+                mCursor = mCursors[i];
+                break;
+            }
+
+            cursorStartPos += mCursors[i].getCount();
+        }
+
+        /* Move it to the right position */
+        if (mCursor != null) {
+            boolean ret = mCursor.moveToPosition(newPosition - cursorStartPos);
+            return ret;
+        }
+        return false;
+    }
+
+    @Override
+    public String getString(int column)
+    {
+        return mCursor.getString(column);
+    }
+
+    @Override
+    public short getShort(int column)
+    {
+        return mCursor.getShort(column);
+    }
+
+    @Override
+    public int getInt(int column)
+    {
+        return mCursor.getInt(column);
+    }
+
+    @Override
+    public long getLong(int column)
+    {
+        return mCursor.getLong(column);
+    }
+
+    @Override
+    public float getFloat(int column)
+    {
+        return mCursor.getFloat(column);
+    }
+
+    @Override
+    public double getDouble(int column)
+    {
+        return mCursor.getDouble(column);
+    }
+
+    @Override
+    public int getType(int column) {
+        return mCursor.getType(column);
+    }
+
+    @Override
+    public boolean isNull(int column)
+    {
+        return mCursor.isNull(column);
+    }
+
+    @Override
+    public byte[] getBlob(int column)
+    {
+        return mCursor.getBlob(column);   
+    }
+
+    @Override
+    public String[] getColumnNames()
+    {
+        if (mCursor != null) {
+            return mCursor.getColumnNames();
+        } else {
+            return new String[0];
+        }
+    }
+    
+    @Override
+    public void deactivate()
+    {
+        int length = mCursors.length;
+        for (int i = 0 ; i < length ; i++) {
+            if (mCursors[i] != null) {
+                mCursors[i].deactivate();
+            }
+        }
+        super.deactivate();
+    }
+
+    @Override
+    public void close() {
+        int length = mCursors.length;
+        for (int i = 0 ; i < length ; i++) {
+            if (mCursors[i] == null) continue;
+            mCursors[i].close();
+        }
+        super.close();
+    }
+
+    @Override
+    public void registerContentObserver(ContentObserver observer) {
+        int length = mCursors.length;
+        for (int i = 0 ; i < length ; i++) {
+            if (mCursors[i] != null) {
+                mCursors[i].registerContentObserver(observer);
+            }
+        }
+    }
+    @Override
+    public void unregisterContentObserver(ContentObserver observer) {
+        int length = mCursors.length;
+        for (int i = 0 ; i < length ; i++) {
+            if (mCursors[i] != null) {
+                mCursors[i].unregisterContentObserver(observer);
+            }
+        }
+    }
+    
+    @Override
+    public void registerDataSetObserver(DataSetObserver observer) {
+        int length = mCursors.length;
+        for (int i = 0 ; i < length ; i++) {
+            if (mCursors[i] != null) {
+                mCursors[i].registerDataSetObserver(observer);
+            }
+        }
+    }
+    
+    @Override
+    public void unregisterDataSetObserver(DataSetObserver observer) {
+        int length = mCursors.length;
+        for (int i = 0 ; i < length ; i++) {
+            if (mCursors[i] != null) {
+                mCursors[i].unregisterDataSetObserver(observer);
+            }
+        }
+    }
+
+    @Override
+    public boolean requery()
+    {
+        int length = mCursors.length;
+        for (int i = 0 ; i < length ; i++) {
+            if (mCursors[i] == null) {
+                continue;
+            }
+
+            if (mCursors[i].requery() == false) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    private Cursor mCursor; // updated in onMove
+    private Cursor[] mCursors;
+}
diff --git a/android/database/Observable.java b/android/database/Observable.java
new file mode 100644
index 0000000..aff32db
--- /dev/null
+++ b/android/database/Observable.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2007 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 android.database;
+
+import java.util.ArrayList;
+
+/**
+ * Provides methods for registering or unregistering arbitrary observers in an {@link ArrayList}.
+ *
+ * This abstract class is intended to be subclassed and specialized to maintain
+ * a registry of observers of specific types and dispatch notifications to them.
+ *
+ * @param T The observer type.
+ */
+public abstract class Observable<T> {
+    /**
+     * The list of observers.  An observer can be in the list at most
+     * once and will never be null.
+     */
+    protected final ArrayList<T> mObservers = new ArrayList<T>();
+
+    /**
+     * Adds an observer to the list. The observer cannot be null and it must not already
+     * be registered.
+     * @param observer the observer to register
+     * @throws IllegalArgumentException the observer is null
+     * @throws IllegalStateException the observer is already registered
+     */
+    public void registerObserver(T observer) {
+        if (observer == null) {
+            throw new IllegalArgumentException("The observer is null.");
+        }
+        synchronized(mObservers) {
+            if (mObservers.contains(observer)) {
+                throw new IllegalStateException("Observer " + observer + " is already registered.");
+            }
+            mObservers.add(observer);
+        }
+    }
+
+    /**
+     * Removes a previously registered observer. The observer must not be null and it
+     * must already have been registered.
+     * @param observer the observer to unregister
+     * @throws IllegalArgumentException the observer is null
+     * @throws IllegalStateException the observer is not yet registered
+     */
+    public void unregisterObserver(T observer) {
+        if (observer == null) {
+            throw new IllegalArgumentException("The observer is null.");
+        }
+        synchronized(mObservers) {
+            int index = mObservers.indexOf(observer);
+            if (index == -1) {
+                throw new IllegalStateException("Observer " + observer + " was not registered.");
+            }
+            mObservers.remove(index);
+        }
+    }
+
+    /**
+     * Remove all registered observers.
+     */
+    public void unregisterAll() {
+        synchronized(mObservers) {
+            mObservers.clear();
+        }
+    }
+}
diff --git a/android/database/RedactingCursor.java b/android/database/RedactingCursor.java
new file mode 100644
index 0000000..6322d56
--- /dev/null
+++ b/android/database/RedactingCursor.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2018 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 android.database;
+
+import android.annotation.NonNull;
+import android.util.SparseArray;
+
+import java.util.Map;
+
+/**
+ * Cursor that offers to redact values of requested columns.
+ *
+ * @hide
+ */
+public class RedactingCursor extends CrossProcessCursorWrapper {
+    private final SparseArray<Object> mRedactions;
+
+    private RedactingCursor(@NonNull Cursor cursor, SparseArray<Object> redactions) {
+        super(cursor);
+        mRedactions = redactions;
+    }
+
+    /**
+     * Create a wrapped instance of the given {@link Cursor} which redacts the
+     * requested columns so they always return specific values when accessed.
+     * <p>
+     * If a redacted column appears multiple times in the underlying cursor, all
+     * instances will be redacted. If none of the redacted columns appear in the
+     * given cursor, the given cursor will be returned untouched to improve
+     * performance.
+     */
+    public static Cursor create(@NonNull Cursor cursor, @NonNull Map<String, Object> redactions) {
+        final SparseArray<Object> internalRedactions = new SparseArray<>();
+
+        final String[] columns = cursor.getColumnNames();
+        for (int i = 0; i < columns.length; i++) {
+            if (redactions.containsKey(columns[i])) {
+                internalRedactions.put(i, redactions.get(columns[i]));
+            }
+        }
+
+        if (internalRedactions.size() == 0) {
+            return cursor;
+        } else {
+            return new RedactingCursor(cursor, internalRedactions);
+        }
+    }
+
+    @Override
+    public void fillWindow(int position, CursorWindow window) {
+        // Fill window directly to ensure data is redacted
+        DatabaseUtils.cursorFillWindow(this, position, window);
+    }
+
+    @Override
+    public CursorWindow getWindow() {
+        // Returning underlying window risks leaking redacted data
+        return null;
+    }
+
+    @Override
+    public Cursor getWrappedCursor() {
+        throw new UnsupportedOperationException(
+                "Returning underlying cursor risks leaking redacted data");
+    }
+
+    @Override
+    public double getDouble(int columnIndex) {
+        final int i = mRedactions.indexOfKey(columnIndex);
+        if (i >= 0) {
+            return (double) mRedactions.valueAt(i);
+        } else {
+            return super.getDouble(columnIndex);
+        }
+    }
+
+    @Override
+    public float getFloat(int columnIndex) {
+        final int i = mRedactions.indexOfKey(columnIndex);
+        if (i >= 0) {
+            return (float) mRedactions.valueAt(i);
+        } else {
+            return super.getFloat(columnIndex);
+        }
+    }
+
+    @Override
+    public int getInt(int columnIndex) {
+        final int i = mRedactions.indexOfKey(columnIndex);
+        if (i >= 0) {
+            return (int) mRedactions.valueAt(i);
+        } else {
+            return super.getInt(columnIndex);
+        }
+    }
+
+    @Override
+    public long getLong(int columnIndex) {
+        final int i = mRedactions.indexOfKey(columnIndex);
+        if (i >= 0) {
+            return (long) mRedactions.valueAt(i);
+        } else {
+            return super.getLong(columnIndex);
+        }
+    }
+
+    @Override
+    public short getShort(int columnIndex) {
+        final int i = mRedactions.indexOfKey(columnIndex);
+        if (i >= 0) {
+            return (short) mRedactions.valueAt(i);
+        } else {
+            return super.getShort(columnIndex);
+        }
+    }
+
+    @Override
+    public String getString(int columnIndex) {
+        final int i = mRedactions.indexOfKey(columnIndex);
+        if (i >= 0) {
+            return (String) mRedactions.valueAt(i);
+        } else {
+            return super.getString(columnIndex);
+        }
+    }
+
+    @Override
+    public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
+        final int i = mRedactions.indexOfKey(columnIndex);
+        if (i >= 0) {
+            buffer.data = ((String) mRedactions.valueAt(i)).toCharArray();
+            buffer.sizeCopied = buffer.data.length;
+        } else {
+            super.copyStringToBuffer(columnIndex, buffer);
+        }
+    }
+
+    @Override
+    public byte[] getBlob(int columnIndex) {
+        final int i = mRedactions.indexOfKey(columnIndex);
+        if (i >= 0) {
+            return (byte[]) mRedactions.valueAt(i);
+        } else {
+            return super.getBlob(columnIndex);
+        }
+    }
+
+    @Override
+    public int getType(int columnIndex) {
+        final int i = mRedactions.indexOfKey(columnIndex);
+        if (i >= 0) {
+            return DatabaseUtils.getTypeOfObject(mRedactions.valueAt(i));
+        } else {
+            return super.getType(columnIndex);
+        }
+    }
+
+    @Override
+    public boolean isNull(int columnIndex) {
+        final int i = mRedactions.indexOfKey(columnIndex);
+        if (i >= 0) {
+            return mRedactions.valueAt(i) == null;
+        } else {
+            return super.isNull(columnIndex);
+        }
+    }
+}
diff --git a/android/database/SQLException.java b/android/database/SQLException.java
new file mode 100644
index 0000000..3402026
--- /dev/null
+++ b/android/database/SQLException.java
@@ -0,0 +1,33 @@
+/*
+ * 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 android.database;
+
+/**
+ * An exception that indicates there was an error with SQL parsing or execution.
+ */
+public class SQLException extends RuntimeException {
+    public SQLException() {
+    }
+
+    public SQLException(String error) {
+        super(error);
+    }
+
+    public SQLException(String error, Throwable cause) {
+        super(error, cause);
+    }
+}
diff --git a/android/database/SQLiteDatabaseIoPerfTest.java b/android/database/SQLiteDatabaseIoPerfTest.java
new file mode 100644
index 0000000..830302e
--- /dev/null
+++ b/android/database/SQLiteDatabaseIoPerfTest.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2017 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 android.database;
+
+import static org.junit.Assert.assertEquals;
+
+import android.app.Activity;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Bundle;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.Preconditions;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Performance tests for measuring amount of data written during typical DB operations
+ *
+ * <p>To run: bit CorePerfTests:android.database.SQLiteDatabaseIoPerfTest
+ */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class SQLiteDatabaseIoPerfTest {
+    private static final String TAG = "SQLiteDatabaseIoPerfTest";
+    private static final String DB_NAME = "db_io_perftest";
+    private static final int DEFAULT_DATASET_SIZE = 500;
+
+    private Long mWriteBytes;
+
+    private SQLiteDatabase mDatabase;
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mContext.deleteDatabase(DB_NAME);
+        mDatabase = mContext.openOrCreateDatabase(DB_NAME, Context.MODE_PRIVATE, null);
+        mDatabase.execSQL("CREATE TABLE T1 "
+                + "(_ID INTEGER PRIMARY KEY, COL_A INTEGER, COL_B VARCHAR(100), COL_C REAL)");
+    }
+
+    @After
+    public void tearDown() {
+        mDatabase.close();
+        mContext.deleteDatabase(DB_NAME);
+    }
+
+    @Test
+    public void testDatabaseModifications() {
+        startMeasuringWrites();
+        ContentValues cv = new ContentValues();
+        String[] whereArg = new String[1];
+        for (int i = 0; i < DEFAULT_DATASET_SIZE; i++) {
+            cv.put("_ID", i);
+            cv.put("COL_A", i);
+            cv.put("COL_B", "NewValue");
+            cv.put("COL_C", 1.0);
+            assertEquals(i, mDatabase.insert("T1", null, cv));
+        }
+        cv = new ContentValues();
+        for (int i = 0; i < DEFAULT_DATASET_SIZE; i++) {
+            cv.put("COL_B", "UpdatedValue");
+            cv.put("COL_C", 1.1);
+            whereArg[0] = String.valueOf(i);
+            assertEquals(1, mDatabase.update("T1", cv, "_ID=?", whereArg));
+        }
+        for (int i = 0; i < DEFAULT_DATASET_SIZE; i++) {
+            whereArg[0] = String.valueOf(i);
+            assertEquals(1, mDatabase.delete("T1", "_ID=?", whereArg));
+        }
+        // Make sure all changes are written to disk
+        mDatabase.close();
+        long bytes = endMeasuringWrites();
+        sendResults("testDatabaseModifications" , bytes);
+    }
+
+    @Test
+    public void testInsertsWithTransactions() {
+        startMeasuringWrites();
+        final int txSize = 10;
+        ContentValues cv = new ContentValues();
+        for (int i = 0; i < DEFAULT_DATASET_SIZE * 5; i++) {
+            if (i % txSize == 0) {
+                mDatabase.beginTransaction();
+            }
+            if (i % txSize == txSize-1) {
+                mDatabase.setTransactionSuccessful();
+                mDatabase.endTransaction();
+
+            }
+            cv.put("_ID", i);
+            cv.put("COL_A", i);
+            cv.put("COL_B", "NewValue");
+            cv.put("COL_C", 1.0);
+            assertEquals(i, mDatabase.insert("T1", null, cv));
+        }
+        // Make sure all changes are written to disk
+        mDatabase.close();
+        long bytes = endMeasuringWrites();
+        sendResults("testInsertsWithTransactions" , bytes);
+    }
+
+    private void startMeasuringWrites() {
+        Preconditions.checkState(mWriteBytes == null, "Measurement already started");
+        mWriteBytes = getIoStats().get("write_bytes");
+    }
+
+    private long endMeasuringWrites() {
+        Preconditions.checkState(mWriteBytes != null, "Measurement wasn't started");
+        Long newWriteBytes = getIoStats().get("write_bytes");
+        return newWriteBytes - mWriteBytes;
+    }
+
+    private void sendResults(String testName, long writeBytes) {
+        Log.i(TAG, testName + " write_bytes: " + writeBytes);
+        Bundle status = new Bundle();
+        status.putLong("write_bytes", writeBytes);
+        InstrumentationRegistry.getInstrumentation().sendStatus(Activity.RESULT_OK, status);
+    }
+
+    private static Map<String, Long> getIoStats() {
+        String ioStat = "/proc/self/io";
+        Map<String, Long> results = new ArrayMap<>();
+        try {
+            List<String> lines = Files.readAllLines(new File(ioStat).toPath());
+            for (String line : lines) {
+                line = line.trim();
+                String[] split = line.split(":");
+                if (split.length == 2) {
+                    try {
+                        String key = split[0].trim();
+                        Long value = Long.valueOf(split[1].trim());
+                        results.put(key, value);
+                    } catch (NumberFormatException e) {
+                        Log.e(TAG, "Cannot parse number from " + line);
+                    }
+                } else if (line.isEmpty()) {
+                    Log.e(TAG, "Cannot parse line " + line);
+                }
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Can't read: " + ioStat, e);
+        }
+        return results;
+    }
+
+}
diff --git a/android/database/SQLiteDatabasePerfTest.java b/android/database/SQLiteDatabasePerfTest.java
new file mode 100644
index 0000000..973e996
--- /dev/null
+++ b/android/database/SQLiteDatabasePerfTest.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2017 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 android.database;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Random;
+
+/**
+ * Performance tests for typical CRUD operations and loading rows into the Cursor
+ *
+ * <p>To run: bit CorePerfTests:android.database.SQLiteDatabasePerfTest
+ */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class SQLiteDatabasePerfTest {
+    // TODO b/64262688 Add Concurrency tests to compare WAL vs DELETE read/write
+    private static final String DB_NAME = "dbperftest";
+    private static final int DEFAULT_DATASET_SIZE = 1000;
+
+    @Rule
+    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+    private SQLiteDatabase mDatabase;
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mContext.deleteDatabase(DB_NAME);
+        mDatabase = mContext.openOrCreateDatabase(DB_NAME, Context.MODE_PRIVATE, null);
+        mDatabase.execSQL("CREATE TABLE T1 "
+                + "(_ID INTEGER PRIMARY KEY, COL_A INTEGER, COL_B VARCHAR(100), COL_C REAL)");
+        mDatabase.execSQL("CREATE TABLE T2 ("
+                + "_ID INTEGER PRIMARY KEY, COL_A VARCHAR(100), T1_ID INTEGER,"
+                + "FOREIGN KEY(T1_ID) REFERENCES T1 (_ID))");
+    }
+
+    @After
+    public void tearDown() {
+        mDatabase.close();
+        mContext.deleteDatabase(DB_NAME);
+    }
+
+    @Test
+    public void testSelect() {
+        insertT1TestDataSet();
+
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+        Random rnd = new Random(0);
+        while (state.keepRunning()) {
+            int index = rnd.nextInt(DEFAULT_DATASET_SIZE);
+            try (Cursor cursor = mDatabase.rawQuery("SELECT _ID, COL_A, COL_B, COL_C FROM T1 "
+                    + "WHERE _ID=?", new String[]{String.valueOf(index)})) {
+                assertTrue(cursor.moveToNext());
+                assertEquals(index, cursor.getInt(0));
+                assertEquals(index, cursor.getInt(1));
+                assertEquals("T1Value" + index, cursor.getString(2));
+                assertEquals(1.1 * index, cursor.getDouble(3), 0.0000001d);
+            }
+        }
+    }
+
+    @Test
+    public void testSelectMultipleRows() {
+        insertT1TestDataSet();
+
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        Random rnd = new Random(0);
+        final int querySize = 50;
+        while (state.keepRunning()) {
+            int index = rnd.nextInt(DEFAULT_DATASET_SIZE - querySize - 1);
+            try (Cursor cursor = mDatabase.rawQuery("SELECT _ID, COL_A, COL_B, COL_C FROM T1 "
+                            + "WHERE _ID BETWEEN ? and ? ORDER BY _ID",
+                    new String[]{String.valueOf(index), String.valueOf(index + querySize - 1)})) {
+                int i = 0;
+                while(cursor.moveToNext()) {
+                    assertEquals(index, cursor.getInt(0));
+                    assertEquals(index, cursor.getInt(1));
+                    assertEquals("T1Value" + index, cursor.getString(2));
+                    assertEquals(1.1 * index, cursor.getDouble(3), 0.0000001d);
+                    index++;
+                    i++;
+                }
+                assertEquals(querySize, i);
+            }
+        }
+    }
+
+    @Test
+    public void testCursorIterateForward() {
+        // A larger dataset is needed to exceed default CursorWindow size
+        int datasetSize = DEFAULT_DATASET_SIZE * 50;
+        insertT1TestDataSet(datasetSize);
+
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            try (Cursor cursor = mDatabase
+                    .rawQuery("SELECT _ID, COL_A, COL_B, COL_C FROM T1 ORDER BY _ID", null)) {
+                int i = 0;
+                while(cursor.moveToNext()) {
+                    assertEquals(i, cursor.getInt(0));
+                    assertEquals(i, cursor.getInt(1));
+                    assertEquals("T1Value" + i, cursor.getString(2));
+                    assertEquals(1.1 * i, cursor.getDouble(3), 0.0000001d);
+                    i++;
+                }
+                assertEquals(datasetSize, i);
+            }
+        }
+    }
+
+    @Test
+    public void testCursorIterateBackwards() {
+        // A larger dataset is needed to exceed default CursorWindow size
+        int datasetSize = DEFAULT_DATASET_SIZE * 50;
+        insertT1TestDataSet(datasetSize);
+
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            try (Cursor cursor = mDatabase
+                    .rawQuery("SELECT _ID, COL_A, COL_B, COL_C FROM T1 ORDER BY _ID", null)) {
+                int i = datasetSize - 1;
+                while(cursor.moveToPosition(i)) {
+                    assertEquals(i, cursor.getInt(0));
+                    assertEquals(i, cursor.getInt(1));
+                    assertEquals("T1Value" + i, cursor.getString(2));
+                    assertEquals(1.1 * i, cursor.getDouble(3), 0.0000001d);
+                    i--;
+                }
+                assertEquals(-1, i);
+            }
+        }
+    }
+
+    @Test
+    public void testInnerJoin() {
+        mDatabase.setForeignKeyConstraintsEnabled(true);
+        mDatabase.beginTransaction();
+        insertT1TestDataSet();
+        insertT2TestDataSet();
+        mDatabase.setTransactionSuccessful();
+        mDatabase.endTransaction();
+
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+        Random rnd = new Random(0);
+        while (state.keepRunning()) {
+            int index = rnd.nextInt(1000);
+            try (Cursor cursor = mDatabase.rawQuery(
+                    "SELECT T1._ID, T1.COL_A, T1.COL_B, T1.COL_C, T2.COL_A FROM T1 "
+                    + "INNER JOIN T2 on T2.T1_ID=T1._ID WHERE T1._ID = ?",
+                    new String[]{String.valueOf(index)})) {
+                assertTrue(cursor.moveToNext());
+                assertEquals(index, cursor.getInt(0));
+                assertEquals(index, cursor.getInt(1));
+                assertEquals("T1Value" + index, cursor.getString(2));
+                assertEquals(1.1 * index, cursor.getDouble(3), 0.0000001d);
+                assertEquals("T2Value" + index, cursor.getString(4));
+            }
+        }
+    }
+
+    @Test
+    public void testInsert() {
+        insertT1TestDataSet();
+
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+        ContentValues cv = new ContentValues();
+        cv.put("_ID", DEFAULT_DATASET_SIZE);
+        cv.put("COL_B", "NewValue");
+        cv.put("COL_C", 1.1);
+        String[] deleteArgs = new String[]{String.valueOf(DEFAULT_DATASET_SIZE)};
+        while (state.keepRunning()) {
+            assertEquals(DEFAULT_DATASET_SIZE, mDatabase.insert("T1", null, cv));
+            state.pauseTiming();
+            assertEquals(1, mDatabase.delete("T1", "_ID=?", deleteArgs));
+            state.resumeTiming();
+        }
+    }
+
+    @Test
+    public void testDelete() {
+        insertT1TestDataSet();
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        String[] deleteArgs = new String[]{String.valueOf(DEFAULT_DATASET_SIZE)};
+        Object[] insertsArgs = new Object[]{DEFAULT_DATASET_SIZE, DEFAULT_DATASET_SIZE,
+                "ValueToDelete", 1.1};
+
+        while (state.keepRunning()) {
+            state.pauseTiming();
+            mDatabase.execSQL("INSERT INTO T1 VALUES (?, ?, ?, ?)", insertsArgs);
+            state.resumeTiming();
+            assertEquals(1, mDatabase.delete("T1", "_ID=?", deleteArgs));
+        }
+    }
+
+    @Test
+    public void testUpdate() {
+        insertT1TestDataSet();
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+        Random rnd = new Random(0);
+        int i = 0;
+        ContentValues cv = new ContentValues();
+        String[] argArray = new String[1];
+        while (state.keepRunning()) {
+            int id = rnd.nextInt(DEFAULT_DATASET_SIZE);
+            cv.put("COL_A", i);
+            cv.put("COL_B", "UpdatedValue");
+            cv.put("COL_C", i);
+            argArray[0] = String.valueOf(id);
+            assertEquals(1, mDatabase.update("T1", cv, "_ID=?", argArray));
+            i++;
+        }
+    }
+
+    private void insertT1TestDataSet() {
+        insertT1TestDataSet(DEFAULT_DATASET_SIZE);
+    }
+
+    private void insertT1TestDataSet(int size) {
+        mDatabase.beginTransaction();
+        for (int i = 0; i < size; i++) {
+            mDatabase.execSQL("INSERT INTO T1 VALUES (?, ?, ?, ?)",
+                    new Object[]{i, i, "T1Value" + i, i * 1.1});
+        }
+        mDatabase.setTransactionSuccessful();
+        mDatabase.endTransaction();
+    }
+
+    private void insertT2TestDataSet() {
+        mDatabase.beginTransaction();
+        for (int i = 0; i < DEFAULT_DATASET_SIZE; i++) {
+            mDatabase.execSQL("INSERT INTO T2 VALUES (?, ?, ?)",
+                    new Object[]{i, "T2Value" + i, i});
+        }
+        mDatabase.setTransactionSuccessful();
+        mDatabase.endTransaction();
+    }
+}
+
diff --git a/android/database/StaleDataException.java b/android/database/StaleDataException.java
new file mode 100644
index 0000000..ee70beb
--- /dev/null
+++ b/android/database/StaleDataException.java
@@ -0,0 +1,34 @@
+/*
+ * 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 android.database;
+
+/**
+ * This exception is thrown when a Cursor contains stale data and must be
+ * requeried before being used again.
+ */
+public class StaleDataException extends java.lang.RuntimeException
+{
+    public StaleDataException()
+    {
+        super();
+    }
+
+    public StaleDataException(String description)
+    {
+        super(description);
+    }
+}
diff --git a/android/database/TableHelper.java b/android/database/TableHelper.java
new file mode 100644
index 0000000..48f3781
--- /dev/null
+++ b/android/database/TableHelper.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2017 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 android.database;
+
+import java.util.Date;
+import java.util.UUID;
+
+/**
+ * Helper class for creating and querying data from a database in performance tests.
+ *
+ * Subclasses can define different table/query formats.
+ */
+public abstract class TableHelper {
+
+    public interface CursorReader {
+        void read();
+    }
+
+    public abstract String createSql();
+    public abstract String insertSql();
+    public abstract Object[] createItem(int id);
+    public abstract String readSql();
+    public abstract CursorReader createReader(Cursor cursor);
+
+    /**
+     * 1 column, single integer
+     */
+    public static TableHelper INT_1 = new TableHelper() {
+        @Override
+        public String createSql() {
+            return "CREATE TABLE `Int1` ("
+                    + "`a` INTEGER,"
+                    + " PRIMARY KEY(`a`))";
+        }
+
+        @Override
+        public String insertSql() {
+            return "INSERT INTO `Int1`(`a`)"
+                    + " VALUES (?)";
+        }
+
+        @Override
+        public Object[] createItem(int id) {
+            return new Object[] {
+                    id,
+            };
+        }
+
+        @Override
+        public String readSql() {
+            return "SELECT * from Int1";
+        }
+
+        @Override
+        public CursorReader createReader(final Cursor cursor) {
+            final int cursorIndexOfA = cursor.getColumnIndexOrThrow("a");
+            return () -> {
+                cursor.getInt(cursorIndexOfA);
+            };
+        }
+    };
+    /**
+     * 10 columns of integers
+     */
+    public static TableHelper INT_10 = new TableHelper() {
+        @Override
+        public String createSql() {
+            return "CREATE TABLE `Int10` ("
+                    + "`a` INTEGER,"
+                    + " `b` INTEGER,"
+                    + " `c` INTEGER,"
+                    + " `d` INTEGER,"
+                    + " `e` INTEGER,"
+                    + " `f` INTEGER,"
+                    + " `g` INTEGER,"
+                    + " `h` INTEGER,"
+                    + " `i` INTEGER,"
+                    + " `j` INTEGER,"
+                    + " PRIMARY KEY(`a`))";
+        }
+
+        @Override
+        public String insertSql() {
+            return "INSERT INTO `Int10`(`a`,`b`,`c`,`d`,`e`,`f`,`g`,`h`,`i`,`j`)"
+                    + " VALUES (?,?,?,?,?,?,?,?,?,?)";
+        }
+
+        @Override
+        public Object[] createItem(int id) {
+            return new Object[] {
+                    id,
+                    id + 1,
+                    id + 2,
+                    id + 3,
+                    id + 4,
+                    id + 5,
+                    id + 6,
+                    id + 7,
+                    id + 8,
+                    id + 9,
+            };
+        }
+
+        @Override
+        public String readSql() {
+            return "SELECT * from Int10";
+        }
+
+        @Override
+        public CursorReader createReader(final Cursor cursor) {
+            final int cursorIndexOfA = cursor.getColumnIndexOrThrow("a");
+            final int cursorIndexOfB = cursor.getColumnIndexOrThrow("b");
+            final int cursorIndexOfC = cursor.getColumnIndexOrThrow("c");
+            final int cursorIndexOfD = cursor.getColumnIndexOrThrow("d");
+            final int cursorIndexOfE = cursor.getColumnIndexOrThrow("e");
+            final int cursorIndexOfF = cursor.getColumnIndexOrThrow("f");
+            final int cursorIndexOfG = cursor.getColumnIndexOrThrow("g");
+            final int cursorIndexOfH = cursor.getColumnIndexOrThrow("h");
+            final int cursorIndexOfI = cursor.getColumnIndexOrThrow("i");
+            final int cursorIndexOfJ = cursor.getColumnIndexOrThrow("j");
+            return () -> {
+                cursor.getInt(cursorIndexOfA);
+                cursor.getInt(cursorIndexOfB);
+                cursor.getInt(cursorIndexOfC);
+                cursor.getInt(cursorIndexOfD);
+                cursor.getInt(cursorIndexOfE);
+                cursor.getInt(cursorIndexOfF);
+                cursor.getInt(cursorIndexOfG);
+                cursor.getInt(cursorIndexOfH);
+                cursor.getInt(cursorIndexOfI);
+                cursor.getInt(cursorIndexOfJ);
+            };
+        }
+    };
+
+    /**
+     * Mock up of 'user' table with various ints/strings
+     */
+    public static TableHelper USER = new TableHelper() {
+        @Override
+        public String createSql() {
+            return "CREATE TABLE `User` ("
+                    + "`mId` INTEGER,"
+                    + " `mName` TEXT,"
+                    + " `mLastName` TEXT,"
+                    + " `mAge` INTEGER,"
+                    + " `mAdmin` INTEGER,"
+                    + " `mWeight` DOUBLE,"
+                    + " `mBirthday` INTEGER,"
+                    + " `mMoreText` TEXT,"
+                    + " PRIMARY KEY(`mId`))";
+        }
+
+        @Override
+        public String insertSql() {
+            return "INSERT INTO `User`(`mId`,`mName`,`mLastName`,`mAge`,"
+                    + "`mAdmin`,`mWeight`,`mBirthday`,`mMoreText`) VALUES (?,?,?,?,?,?,?,?)";
+        }
+
+        @Override
+        public Object[] createItem(int id) {
+            return new Object[] {
+                    id,
+                    UUID.randomUUID().toString(),
+                    UUID.randomUUID().toString(),
+                    (int) (10 + Math.random() * 50),
+                    0,
+                    (float)0,
+                    new Date().getTime(),
+                    UUID.randomUUID().toString(),
+            };
+        }
+
+        @Override
+        public String readSql() {
+            return "SELECT * from User";
+        }
+
+        @Override
+        public CursorReader createReader(final Cursor cursor) {
+            final int cursorIndexOfMId = cursor.getColumnIndexOrThrow("mId");
+            final int cursorIndexOfMName = cursor.getColumnIndexOrThrow("mName");
+            final int cursorIndexOfMLastName = cursor.getColumnIndexOrThrow("mLastName");
+            final int cursorIndexOfMAge = cursor.getColumnIndexOrThrow("mAge");
+            final int cursorIndexOfMAdmin = cursor.getColumnIndexOrThrow("mAdmin");
+            final int cursorIndexOfMWeight = cursor.getColumnIndexOrThrow("mWeight");
+            final int cursorIndexOfMBirthday = cursor.getColumnIndexOrThrow("mBirthday");
+            final int cursorIndexOfMMoreTextField = cursor.getColumnIndexOrThrow("mMoreText");
+            return () -> {
+                cursor.getInt(cursorIndexOfMId);
+                cursor.getString(cursorIndexOfMName);
+                cursor.getString(cursorIndexOfMLastName);
+                cursor.getInt(cursorIndexOfMAge);
+                cursor.getInt(cursorIndexOfMAdmin);
+                cursor.getFloat(cursorIndexOfMWeight);
+                if (!cursor.isNull(cursorIndexOfMBirthday)) {
+                    cursor.getLong(cursorIndexOfMBirthday);
+                }
+                cursor.getString(cursorIndexOfMMoreTextField);
+            };
+        }
+    };
+
+    public static TableHelper[] TABLE_HELPERS = new TableHelper[] {
+            INT_1,
+            INT_10,
+            USER,
+    };
+}
diff --git a/android/database/TranslatingCursor.java b/android/database/TranslatingCursor.java
new file mode 100644
index 0000000..35cbdc7
--- /dev/null
+++ b/android/database/TranslatingCursor.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2018 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 android.database;
+
+import android.annotation.NonNull;
+import android.content.ContentResolver;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.CancellationSignal;
+import android.util.ArraySet;
+
+import com.android.internal.util.ArrayUtils;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Cursor that supports deprecation of {@code _data} like columns which represent raw filepaths,
+ * typically by replacing values with fake paths that the OS then offers to redirect to
+ * {@link ContentResolver#openFileDescriptor(Uri, String)}, which developers
+ * should be using directly.
+ *
+ * @hide
+ */
+public class TranslatingCursor extends CrossProcessCursorWrapper {
+    public static class Config {
+        public final Uri baseUri;
+        public final String auxiliaryColumn;
+        public final String[] translateColumns;
+
+        public Config(Uri baseUri, String auxiliaryColumn, String... translateColumns) {
+            this.baseUri = baseUri;
+            this.auxiliaryColumn = auxiliaryColumn;
+            this.translateColumns = translateColumns;
+        }
+    }
+
+    public interface Translator {
+        String translate(String data, int auxiliaryColumnIndex,
+                String matchingColumn, Cursor cursor);
+    }
+
+    private final @NonNull Config mConfig;
+    private final @NonNull Translator mTranslator;
+    private final boolean mDropLast;
+
+    private final int mAuxiliaryColumnIndex;
+    private final ArraySet<Integer> mTranslateColumnIndices;
+
+    public TranslatingCursor(@NonNull Cursor cursor, @NonNull Config config,
+            @NonNull Translator translator, boolean dropLast) {
+        super(cursor);
+
+        mConfig = Objects.requireNonNull(config);
+        mTranslator = Objects.requireNonNull(translator);
+        mDropLast = dropLast;
+
+        mAuxiliaryColumnIndex = cursor.getColumnIndexOrThrow(config.auxiliaryColumn);
+        mTranslateColumnIndices = new ArraySet<>();
+        for (int i = 0; i < cursor.getColumnCount(); ++i) {
+            String columnName = cursor.getColumnName(i);
+            if (ArrayUtils.contains(config.translateColumns, columnName)) {
+                mTranslateColumnIndices.add(i);
+            }
+        }
+    }
+
+    @Override
+    public int getColumnCount() {
+        if (mDropLast) {
+            return super.getColumnCount() - 1;
+        } else {
+            return super.getColumnCount();
+        }
+    }
+
+    @Override
+    public String[] getColumnNames() {
+        if (mDropLast) {
+            return Arrays.copyOfRange(super.getColumnNames(), 0, super.getColumnCount() - 1);
+        } else {
+            return super.getColumnNames();
+        }
+    }
+
+    public static Cursor query(@NonNull Config config, @NonNull Translator translator,
+            SQLiteQueryBuilder qb, SQLiteDatabase db, String[] projectionIn, String selection,
+            String[] selectionArgs, String groupBy, String having, String sortOrder, String limit,
+            CancellationSignal signal) {
+        final boolean requestedAuxiliaryColumn = ArrayUtils.isEmpty(projectionIn)
+                || ArrayUtils.contains(projectionIn, config.auxiliaryColumn);
+        final boolean requestedTranslateColumns = ArrayUtils.isEmpty(projectionIn)
+                || ArrayUtils.containsAny(projectionIn, config.translateColumns);
+
+        // If caller didn't request any columns that need to be translated,
+        // we have nothing to redirect
+        if (!requestedTranslateColumns) {
+            return qb.query(db, projectionIn, selection, selectionArgs,
+                    groupBy, having, sortOrder, limit, signal);
+        }
+
+        // If caller didn't request auxiliary column, we need to splice it in
+        if (!requestedAuxiliaryColumn) {
+            projectionIn = ArrayUtils.appendElement(String.class, projectionIn,
+                    config.auxiliaryColumn);
+        }
+
+        final Cursor c = qb.query(db, projectionIn, selection, selectionArgs,
+                groupBy, having, sortOrder);
+        return new TranslatingCursor(c, config, translator, !requestedAuxiliaryColumn);
+    }
+
+    @Override
+    public void fillWindow(int position, CursorWindow window) {
+        // Fill window directly to ensure data is rewritten
+        DatabaseUtils.cursorFillWindow(this, position, window);
+    }
+
+    @Override
+    public CursorWindow getWindow() {
+        // Returning underlying window risks leaking data
+        return null;
+    }
+
+    @Override
+    public Cursor getWrappedCursor() {
+        throw new UnsupportedOperationException(
+                "Returning underlying cursor risks leaking data");
+    }
+
+    @Override
+    public double getDouble(int columnIndex) {
+        if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) {
+            throw new IllegalArgumentException();
+        } else {
+            return super.getDouble(columnIndex);
+        }
+    }
+
+    @Override
+    public float getFloat(int columnIndex) {
+        if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) {
+            throw new IllegalArgumentException();
+        } else {
+            return super.getFloat(columnIndex);
+        }
+    }
+
+    @Override
+    public int getInt(int columnIndex) {
+        if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) {
+            throw new IllegalArgumentException();
+        } else {
+            return super.getInt(columnIndex);
+        }
+    }
+
+    @Override
+    public long getLong(int columnIndex) {
+        if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) {
+            throw new IllegalArgumentException();
+        } else {
+            return super.getLong(columnIndex);
+        }
+    }
+
+    @Override
+    public short getShort(int columnIndex) {
+        if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) {
+            throw new IllegalArgumentException();
+        } else {
+            return super.getShort(columnIndex);
+        }
+    }
+
+    @Override
+    public String getString(int columnIndex) {
+        if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) {
+            return mTranslator.translate(super.getString(columnIndex),
+                    mAuxiliaryColumnIndex, getColumnName(columnIndex), this);
+        } else {
+            return super.getString(columnIndex);
+        }
+    }
+
+    @Override
+    public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
+        if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) {
+            throw new IllegalArgumentException();
+        } else {
+            super.copyStringToBuffer(columnIndex, buffer);
+        }
+    }
+
+    @Override
+    public byte[] getBlob(int columnIndex) {
+        if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) {
+            throw new IllegalArgumentException();
+        } else {
+            return super.getBlob(columnIndex);
+        }
+    }
+
+    @Override
+    public int getType(int columnIndex) {
+        if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) {
+            return Cursor.FIELD_TYPE_STRING;
+        } else {
+            return super.getType(columnIndex);
+        }
+    }
+
+    @Override
+    public boolean isNull(int columnIndex) {
+        if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) {
+            return getString(columnIndex) == null;
+        } else {
+            return super.isNull(columnIndex);
+        }
+    }
+}
diff --git a/android/database/sqlite/DatabaseObjectNotClosedException.java b/android/database/sqlite/DatabaseObjectNotClosedException.java
new file mode 100644
index 0000000..ba546f3
--- /dev/null
+++ b/android/database/sqlite/DatabaseObjectNotClosedException.java
@@ -0,0 +1,34 @@
+/*
+ * 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 android.database.sqlite;
+
+import android.compat.annotation.UnsupportedAppUsage;
+
+/**
+ * An exception that indicates that garbage-collector is finalizing a database object
+ * that is not explicitly closed
+ * @hide
+ */
+public class DatabaseObjectNotClosedException extends RuntimeException {
+    private static final String s = "Application did not close the cursor or database object " +
+            "that was opened here";
+
+    @UnsupportedAppUsage
+    public DatabaseObjectNotClosedException() {
+        super(s);
+    }
+}
diff --git a/android/database/sqlite/SQLiteAbortException.java b/android/database/sqlite/SQLiteAbortException.java
new file mode 100644
index 0000000..64dc4b7
--- /dev/null
+++ b/android/database/sqlite/SQLiteAbortException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2008 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 android.database.sqlite;
+
+/**
+ * An exception that indicates that the SQLite program was aborted.
+ * This can happen either through a call to ABORT in a trigger,
+ * or as the result of using the ABORT conflict clause.
+ */
+public class SQLiteAbortException extends SQLiteException {
+    public SQLiteAbortException() {}
+
+    public SQLiteAbortException(String error) {
+        super(error);
+    }
+}
diff --git a/android/database/sqlite/SQLiteAccessPermException.java b/android/database/sqlite/SQLiteAccessPermException.java
new file mode 100644
index 0000000..238da4b
--- /dev/null
+++ b/android/database/sqlite/SQLiteAccessPermException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2010 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 android.database.sqlite;
+
+/**
+ * This exception class is used when sqlite can't access the database file
+ * due to lack of permissions on the file.
+ */
+public class SQLiteAccessPermException extends SQLiteException {
+    public SQLiteAccessPermException() {}
+
+    public SQLiteAccessPermException(String error) {
+        super(error);
+    }
+}
diff --git a/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java b/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java
new file mode 100644
index 0000000..41f2f9c
--- /dev/null
+++ b/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2010 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 android.database.sqlite;
+
+/**
+ * Thrown if the the bind or column parameter index is out of range
+ */
+public class SQLiteBindOrColumnIndexOutOfRangeException extends SQLiteException {
+    public SQLiteBindOrColumnIndexOutOfRangeException() {}
+
+    public SQLiteBindOrColumnIndexOutOfRangeException(String error) {
+        super(error);
+    }
+}
diff --git a/android/database/sqlite/SQLiteBlobTooBigException.java b/android/database/sqlite/SQLiteBlobTooBigException.java
new file mode 100644
index 0000000..a82676b
--- /dev/null
+++ b/android/database/sqlite/SQLiteBlobTooBigException.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2010 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 android.database.sqlite;
+
+public class SQLiteBlobTooBigException extends SQLiteException {
+    public SQLiteBlobTooBigException() {}
+
+    public SQLiteBlobTooBigException(String error) {
+        super(error);
+    }
+}
diff --git a/android/database/sqlite/SQLiteCantOpenDatabaseException.java b/android/database/sqlite/SQLiteCantOpenDatabaseException.java
new file mode 100644
index 0000000..5d4b48d
--- /dev/null
+++ b/android/database/sqlite/SQLiteCantOpenDatabaseException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2010 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 android.database.sqlite;
+
+public class SQLiteCantOpenDatabaseException extends SQLiteException {
+    public SQLiteCantOpenDatabaseException() {}
+
+    public SQLiteCantOpenDatabaseException(String error) {
+        super(error);
+    }
+
+    /** @hide */
+    public SQLiteCantOpenDatabaseException(String error, Throwable cause) {
+        super(error, cause);
+    }
+}
diff --git a/android/database/sqlite/SQLiteClosable.java b/android/database/sqlite/SQLiteClosable.java
new file mode 100644
index 0000000..2fca729
--- /dev/null
+++ b/android/database/sqlite/SQLiteClosable.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2007 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 android.database.sqlite;
+
+import android.compat.annotation.UnsupportedAppUsage;
+
+import java.io.Closeable;
+
+/**
+ * An object created from a SQLiteDatabase that can be closed.
+ *
+ * This class implements a primitive reference counting scheme for database objects.
+ */
+public abstract class SQLiteClosable implements Closeable {
+    @UnsupportedAppUsage
+    private int mReferenceCount = 1;
+
+    /**
+     * Called when the last reference to the object was released by
+     * a call to {@link #releaseReference()} or {@link #close()}.
+     */
+    protected abstract void onAllReferencesReleased();
+
+    /**
+     * Called when the last reference to the object was released by
+     * a call to {@link #releaseReferenceFromContainer()}.
+     *
+     * @deprecated Do not use.
+     */
+    @Deprecated
+    protected void onAllReferencesReleasedFromContainer() {
+        onAllReferencesReleased();
+    }
+
+    /**
+     * Acquires a reference to the object.
+     *
+     * @throws IllegalStateException if the last reference to the object has already
+     * been released.
+     */
+    public void acquireReference() {
+        synchronized(this) {
+            if (mReferenceCount <= 0) {
+                throw new IllegalStateException(
+                        "attempt to re-open an already-closed object: " + this);
+            }
+            mReferenceCount++;
+        }
+    }
+
+    /**
+     * Releases a reference to the object, closing the object if the last reference
+     * was released.
+     *
+     * @see #onAllReferencesReleased()
+     */
+    public void releaseReference() {
+        boolean refCountIsZero = false;
+        synchronized(this) {
+            refCountIsZero = --mReferenceCount == 0;
+        }
+        if (refCountIsZero) {
+            onAllReferencesReleased();
+        }
+    }
+
+    /**
+     * Releases a reference to the object that was owned by the container of the object,
+     * closing the object if the last reference was released.
+     *
+     * @see #onAllReferencesReleasedFromContainer()
+     * @deprecated Do not use.
+     */
+    @Deprecated
+    public void releaseReferenceFromContainer() {
+        boolean refCountIsZero = false;
+        synchronized(this) {
+            refCountIsZero = --mReferenceCount == 0;
+        }
+        if (refCountIsZero) {
+            onAllReferencesReleasedFromContainer();
+        }
+    }
+
+    /**
+     * Releases a reference to the object, closing the object if the last reference
+     * was released.
+     *
+     * Calling this method is equivalent to calling {@link #releaseReference}.
+     *
+     * @see #releaseReference()
+     * @see #onAllReferencesReleased()
+     */
+    public void close() {
+        releaseReference();
+    }
+}
diff --git a/android/database/sqlite/SQLiteCompatibilityWalFlags.java b/android/database/sqlite/SQLiteCompatibilityWalFlags.java
new file mode 100644
index 0000000..a269072
--- /dev/null
+++ b/android/database/sqlite/SQLiteCompatibilityWalFlags.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2017 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 android.database.sqlite;
+
+import android.annotation.TestApi;
+import android.app.ActivityThread;
+import android.app.Application;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.KeyValueListParser;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Helper class for accessing
+ * {@link Settings.Global#SQLITE_COMPATIBILITY_WAL_FLAGS global compatibility WAL settings}.
+ *
+ * <p>The value of {@link Settings.Global#SQLITE_COMPATIBILITY_WAL_FLAGS} is cached on first access
+ * for consistent behavior across all connections opened in the process.
+ * @hide
+ */
+@TestApi
+public class SQLiteCompatibilityWalFlags {
+
+    private static final String TAG = "SQLiteCompatibilityWalFlags";
+
+    private static volatile boolean sInitialized;
+    private static volatile boolean sLegacyCompatibilityWalEnabled;
+    private static volatile String sWALSyncMode;
+    private static volatile long sTruncateSize = -1;
+    // This flag is used to avoid recursive initialization due to circular dependency on Settings
+    private static volatile boolean sCallingGlobalSettings;
+
+    private SQLiteCompatibilityWalFlags() {
+    }
+
+    /**
+     * @hide
+     */
+    @VisibleForTesting
+    public static boolean isLegacyCompatibilityWalEnabled() {
+        initIfNeeded();
+        return sLegacyCompatibilityWalEnabled;
+    }
+
+    /**
+     * @hide
+     */
+    @VisibleForTesting
+    public static String getWALSyncMode() {
+        initIfNeeded();
+        // The configurable WAL sync mode should only ever be used if the legacy compatibility
+        // WAL is enabled. It should *not* have any effect if app developers explicitly turn on
+        // WAL for their database using setWriteAheadLoggingEnabled. Throwing an exception here
+        // adds an extra layer of checking that we never use it in the wrong place.
+        if (!sLegacyCompatibilityWalEnabled) {
+            throw new IllegalStateException("isLegacyCompatibilityWalEnabled() == false");
+        }
+
+        return sWALSyncMode;
+    }
+
+    /**
+     * Override {@link com.android.internal.R.integer#db_wal_truncate_size}.
+     *
+     * @return the value set in the global setting, or -1 if a value is not set.
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    public static long getTruncateSize() {
+        initIfNeeded();
+        return sTruncateSize;
+    }
+
+    private static void initIfNeeded() {
+        if (sInitialized || sCallingGlobalSettings) {
+            return;
+        }
+        ActivityThread activityThread = ActivityThread.currentActivityThread();
+        Application app = activityThread == null ? null : activityThread.getApplication();
+        String flags = null;
+        if (app == null) {
+            Log.w(TAG, "Cannot read global setting "
+                    + Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS + " - "
+                    + "Application state not available");
+        } else {
+            try {
+                sCallingGlobalSettings = true;
+                flags = Settings.Global.getString(app.getContentResolver(),
+                        Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS);
+            } finally {
+                sCallingGlobalSettings = false;
+            }
+        }
+
+        init(flags);
+    }
+
+    /**
+     * @hide
+     */
+    @VisibleForTesting
+    public static void init(String flags) {
+        if (TextUtils.isEmpty(flags)) {
+            sInitialized = true;
+            return;
+        }
+        KeyValueListParser parser = new KeyValueListParser(',');
+        try {
+            parser.setString(flags);
+        } catch (IllegalArgumentException e) {
+            Log.e(TAG, "Setting has invalid format: " + flags, e);
+            sInitialized = true;
+            return;
+        }
+        sLegacyCompatibilityWalEnabled = parser.getBoolean(
+                "legacy_compatibility_wal_enabled", false);
+        sWALSyncMode = parser.getString("wal_syncmode", SQLiteGlobal.getWALSyncMode());
+        sTruncateSize = parser.getInt("truncate_size", -1);
+        Log.i(TAG, "Read compatibility WAL flags: legacy_compatibility_wal_enabled="
+                + sLegacyCompatibilityWalEnabled + ", wal_syncmode=" + sWALSyncMode);
+        sInitialized = true;
+    }
+
+    /**
+     * @hide
+     */
+    @VisibleForTesting
+    @TestApi
+    public static void reset() {
+        sInitialized = false;
+        sLegacyCompatibilityWalEnabled = false;
+        sWALSyncMode = null;
+    }
+}
diff --git a/android/database/sqlite/SQLiteConnection.java b/android/database/sqlite/SQLiteConnection.java
new file mode 100644
index 0000000..2f67f6d
--- /dev/null
+++ b/android/database/sqlite/SQLiteConnection.java
@@ -0,0 +1,1719 @@
+/*
+ * Copyright (C) 2011 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 android.database.sqlite;
+
+import android.database.Cursor;
+import android.database.CursorWindow;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDebug.DbStats;
+import android.database.sqlite.SQLiteDebug.NoPreloadHolder;
+import android.os.CancellationSignal;
+import android.os.OperationCanceledException;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.util.Log;
+import android.util.LruCache;
+import android.util.Pair;
+import android.util.Printer;
+
+import dalvik.system.BlockGuard;
+import dalvik.system.CloseGuard;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Map;
+import java.util.function.BinaryOperator;
+import java.util.function.UnaryOperator;
+
+/**
+ * Represents a SQLite database connection.
+ * Each connection wraps an instance of a native <code>sqlite3</code> object.
+ * <p>
+ * When database connection pooling is enabled, there can be multiple active
+ * connections to the same database.  Otherwise there is typically only one
+ * connection per database.
+ * </p><p>
+ * When the SQLite WAL feature is enabled, multiple readers and one writer
+ * can concurrently access the database.  Without WAL, readers and writers
+ * are mutually exclusive.
+ * </p>
+ *
+ * <h2>Ownership and concurrency guarantees</h2>
+ * <p>
+ * Connection objects are not thread-safe.  They are acquired as needed to
+ * perform a database operation and are then returned to the pool.  At any
+ * given time, a connection is either owned and used by a {@link SQLiteSession}
+ * object or the {@link SQLiteConnectionPool}.  Those classes are
+ * responsible for serializing operations to guard against concurrent
+ * use of a connection.
+ * </p><p>
+ * The guarantee of having a single owner allows this class to be implemented
+ * without locks and greatly simplifies resource management.
+ * </p>
+ *
+ * <h2>Encapsulation guarantees</h2>
+ * <p>
+ * The connection object object owns *all* of the SQLite related native
+ * objects that are associated with the connection.  What's more, there are
+ * no other objects in the system that are capable of obtaining handles to
+ * those native objects.  Consequently, when the connection is closed, we do
+ * not have to worry about what other components might have references to
+ * its associated SQLite state -- there are none.
+ * </p><p>
+ * Encapsulation is what ensures that the connection object's
+ * lifecycle does not become a tortured mess of finalizers and reference
+ * queues.
+ * </p>
+ *
+ * <h2>Reentrance</h2>
+ * <p>
+ * This class must tolerate reentrant execution of SQLite operations because
+ * triggers may call custom SQLite functions that perform additional queries.
+ * </p>
+ *
+ * @hide
+ */
+public final class SQLiteConnection implements CancellationSignal.OnCancelListener {
+    private static final String TAG = "SQLiteConnection";
+    private static final boolean DEBUG = false;
+
+    private static final String[] EMPTY_STRING_ARRAY = new String[0];
+    private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+
+    private final CloseGuard mCloseGuard = CloseGuard.get();
+
+    private final SQLiteConnectionPool mPool;
+    private final SQLiteDatabaseConfiguration mConfiguration;
+    private final int mConnectionId;
+    private final boolean mIsPrimaryConnection;
+    private final boolean mIsReadOnlyConnection;
+    private final PreparedStatementCache mPreparedStatementCache;
+    private PreparedStatement mPreparedStatementPool;
+
+    // The recent operations log.
+    private final OperationLog mRecentOperations;
+
+    // The native SQLiteConnection pointer.  (FOR INTERNAL USE ONLY)
+    private long mConnectionPtr;
+
+    private boolean mOnlyAllowReadOnlyOperations;
+
+    // The number of times attachCancellationSignal has been called.
+    // Because SQLite statement execution can be reentrant, we keep track of how many
+    // times we have attempted to attach a cancellation signal to the connection so that
+    // we can ensure that we detach the signal at the right time.
+    private int mCancellationSignalAttachCount;
+
+    private static native long nativeOpen(String path, int openFlags, String label,
+            boolean enableTrace, boolean enableProfile, int lookasideSlotSize,
+            int lookasideSlotCount);
+    private static native void nativeClose(long connectionPtr);
+    private static native void nativeRegisterCustomScalarFunction(long connectionPtr,
+            String name, UnaryOperator<String> function);
+    private static native void nativeRegisterCustomAggregateFunction(long connectionPtr,
+            String name, BinaryOperator<String> function);
+    private static native void nativeRegisterLocalizedCollators(long connectionPtr, String locale);
+    private static native long nativePrepareStatement(long connectionPtr, String sql);
+    private static native void nativeFinalizeStatement(long connectionPtr, long statementPtr);
+    private static native int nativeGetParameterCount(long connectionPtr, long statementPtr);
+    private static native boolean nativeIsReadOnly(long connectionPtr, long statementPtr);
+    private static native int nativeGetColumnCount(long connectionPtr, long statementPtr);
+    private static native String nativeGetColumnName(long connectionPtr, long statementPtr,
+            int index);
+    private static native void nativeBindNull(long connectionPtr, long statementPtr,
+            int index);
+    private static native void nativeBindLong(long connectionPtr, long statementPtr,
+            int index, long value);
+    private static native void nativeBindDouble(long connectionPtr, long statementPtr,
+            int index, double value);
+    private static native void nativeBindString(long connectionPtr, long statementPtr,
+            int index, String value);
+    private static native void nativeBindBlob(long connectionPtr, long statementPtr,
+            int index, byte[] value);
+    private static native void nativeResetStatementAndClearBindings(
+            long connectionPtr, long statementPtr);
+    private static native void nativeExecute(long connectionPtr, long statementPtr);
+    private static native long nativeExecuteForLong(long connectionPtr, long statementPtr);
+    private static native String nativeExecuteForString(long connectionPtr, long statementPtr);
+    private static native int nativeExecuteForBlobFileDescriptor(
+            long connectionPtr, long statementPtr);
+    private static native int nativeExecuteForChangedRowCount(long connectionPtr, long statementPtr);
+    private static native long nativeExecuteForLastInsertedRowId(
+            long connectionPtr, long statementPtr);
+    private static native long nativeExecuteForCursorWindow(
+            long connectionPtr, long statementPtr, long windowPtr,
+            int startPos, int requiredPos, boolean countAllRows);
+    private static native int nativeGetDbLookaside(long connectionPtr);
+    private static native void nativeCancel(long connectionPtr);
+    private static native void nativeResetCancel(long connectionPtr, boolean cancelable);
+
+    private SQLiteConnection(SQLiteConnectionPool pool,
+            SQLiteDatabaseConfiguration configuration,
+            int connectionId, boolean primaryConnection) {
+        mPool = pool;
+        mRecentOperations = new OperationLog(mPool);
+        mConfiguration = new SQLiteDatabaseConfiguration(configuration);
+        mConnectionId = connectionId;
+        mIsPrimaryConnection = primaryConnection;
+        mIsReadOnlyConnection = (configuration.openFlags & SQLiteDatabase.OPEN_READONLY) != 0;
+        mPreparedStatementCache = new PreparedStatementCache(
+                mConfiguration.maxSqlCacheSize);
+        mCloseGuard.open("close");
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            if (mPool != null && mConnectionPtr != 0) {
+                mPool.onConnectionLeaked();
+            }
+
+            dispose(true);
+        } finally {
+            super.finalize();
+        }
+    }
+
+    // Called by SQLiteConnectionPool only.
+    static SQLiteConnection open(SQLiteConnectionPool pool,
+            SQLiteDatabaseConfiguration configuration,
+            int connectionId, boolean primaryConnection) {
+        SQLiteConnection connection = new SQLiteConnection(pool, configuration,
+                connectionId, primaryConnection);
+        try {
+            connection.open();
+            return connection;
+        } catch (SQLiteException ex) {
+            connection.dispose(false);
+            throw ex;
+        }
+    }
+
+    // Called by SQLiteConnectionPool only.
+    // Closes the database closes and releases all of its associated resources.
+    // Do not call methods on the connection after it is closed.  It will probably crash.
+    void close() {
+        dispose(false);
+    }
+
+    private void open() {
+        final String file = mConfiguration.path;
+        final int cookie = mRecentOperations.beginOperation("open", null, null);
+        try {
+            mConnectionPtr = nativeOpen(file, mConfiguration.openFlags,
+                    mConfiguration.label,
+                    NoPreloadHolder.DEBUG_SQL_STATEMENTS, NoPreloadHolder.DEBUG_SQL_TIME,
+                    mConfiguration.lookasideSlotSize, mConfiguration.lookasideSlotCount);
+        } catch (SQLiteCantOpenDatabaseException e) {
+            String message = String.format("Cannot open database '%s'", file);
+
+            try {
+                // Try to diagnose for common reasons. If something fails in here, that's fine;
+                // just swallow the exception.
+
+                final Path path = FileSystems.getDefault().getPath(file);
+                final Path dir = path.getParent();
+
+                if (!Files.isDirectory(dir)) {
+                    message += ": Directory " + dir + " doesn't exist";
+                } else if (!Files.exists(path)) {
+                    message += ": File " + path + " doesn't exist";
+                } else if (!Files.isReadable(path)) {
+                    message += ": File " + path + " is not readable";
+                } else if (Files.isDirectory(path)) {
+                    message += ": Path " + path + " is a directory";
+                } else {
+                    message += ": Unknown reason";
+                }
+            } catch (Throwable th) {
+                message += ": Unknown reason; cannot examine filesystem: " + th.getMessage();
+            }
+            throw new SQLiteCantOpenDatabaseException(message, e);
+        } finally {
+            mRecentOperations.endOperation(cookie);
+        }
+        setPageSize();
+        setForeignKeyModeFromConfiguration();
+        setWalModeFromConfiguration();
+        setJournalSizeLimit();
+        setAutoCheckpointInterval();
+        setLocaleFromConfiguration();
+        setCustomFunctionsFromConfiguration();
+        executePerConnectionSqlFromConfiguration(0);
+    }
+
+    private void dispose(boolean finalized) {
+        if (mCloseGuard != null) {
+            if (finalized) {
+                mCloseGuard.warnIfOpen();
+            }
+            mCloseGuard.close();
+        }
+
+        if (mConnectionPtr != 0) {
+            final int cookie = mRecentOperations.beginOperation("close", null, null);
+            try {
+                mPreparedStatementCache.evictAll();
+                nativeClose(mConnectionPtr);
+                mConnectionPtr = 0;
+            } finally {
+                mRecentOperations.endOperation(cookie);
+            }
+        }
+    }
+
+    private void setPageSize() {
+        if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) {
+            final long newValue = SQLiteGlobal.getDefaultPageSize();
+            long value = executeForLong("PRAGMA page_size", null, null);
+            if (value != newValue) {
+                execute("PRAGMA page_size=" + newValue, null, null);
+            }
+        }
+    }
+
+    private void setAutoCheckpointInterval() {
+        if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) {
+            final long newValue = SQLiteGlobal.getWALAutoCheckpoint();
+            long value = executeForLong("PRAGMA wal_autocheckpoint", null, null);
+            if (value != newValue) {
+                executeForLong("PRAGMA wal_autocheckpoint=" + newValue, null, null);
+            }
+        }
+    }
+
+    private void setJournalSizeLimit() {
+        if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) {
+            final long newValue = SQLiteGlobal.getJournalSizeLimit();
+            long value = executeForLong("PRAGMA journal_size_limit", null, null);
+            if (value != newValue) {
+                executeForLong("PRAGMA journal_size_limit=" + newValue, null, null);
+            }
+        }
+    }
+
+    private void setForeignKeyModeFromConfiguration() {
+        if (!mIsReadOnlyConnection) {
+            final long newValue = mConfiguration.foreignKeyConstraintsEnabled ? 1 : 0;
+            long value = executeForLong("PRAGMA foreign_keys", null, null);
+            if (value != newValue) {
+                execute("PRAGMA foreign_keys=" + newValue, null, null);
+            }
+        }
+    }
+
+    private void setWalModeFromConfiguration() {
+        if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) {
+            final boolean walEnabled =
+                    (mConfiguration.openFlags & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0;
+            // Use compatibility WAL unless an app explicitly set journal/synchronous mode
+            // or DISABLE_COMPATIBILITY_WAL flag is set
+            final boolean isCompatibilityWalEnabled =
+                    mConfiguration.isLegacyCompatibilityWalEnabled();
+            if (walEnabled || isCompatibilityWalEnabled) {
+                setJournalMode("WAL");
+                if (mConfiguration.syncMode != null) {
+                    setSyncMode(mConfiguration.syncMode);
+                } else if (isCompatibilityWalEnabled) {
+                    setSyncMode(SQLiteCompatibilityWalFlags.getWALSyncMode());
+                } else {
+                    setSyncMode(SQLiteGlobal.getWALSyncMode());
+                }
+                maybeTruncateWalFile();
+            } else {
+                setJournalMode(mConfiguration.journalMode == null
+                        ? SQLiteGlobal.getDefaultJournalMode() : mConfiguration.journalMode);
+                setSyncMode(mConfiguration.syncMode == null
+                        ? SQLiteGlobal.getDefaultSyncMode() : mConfiguration.syncMode);
+            }
+        }
+    }
+
+    /**
+     * If the WAL file exists and larger than a threshold, truncate it by executing
+     * PRAGMA wal_checkpoint.
+     */
+    private void maybeTruncateWalFile() {
+        final long threshold = SQLiteGlobal.getWALTruncateSize();
+        if (DEBUG) {
+            Log.d(TAG, "Truncate threshold=" + threshold);
+        }
+        if (threshold == 0) {
+            return;
+        }
+
+        final File walFile = new File(mConfiguration.path + "-wal");
+        if (!walFile.isFile()) {
+            return;
+        }
+        final long size = walFile.length();
+        if (size < threshold) {
+            if (DEBUG) {
+                Log.d(TAG, walFile.getAbsolutePath() + " " + size + " bytes: No need to truncate");
+            }
+            return;
+        }
+
+        Log.i(TAG, walFile.getAbsolutePath() + " " + size + " bytes: Bigger than "
+                + threshold + "; truncating");
+        try {
+            executeForString("PRAGMA wal_checkpoint(TRUNCATE)", null, null);
+        } catch (SQLiteException e) {
+            Log.w(TAG, "Failed to truncate the -wal file", e);
+        }
+    }
+
+    private void setSyncMode(String newValue) {
+        String value = executeForString("PRAGMA synchronous", null, null);
+        if (!canonicalizeSyncMode(value).equalsIgnoreCase(
+                canonicalizeSyncMode(newValue))) {
+            execute("PRAGMA synchronous=" + newValue, null, null);
+        }
+    }
+
+    private static String canonicalizeSyncMode(String value) {
+        switch (value) {
+            case "0": return "OFF";
+            case "1": return "NORMAL";
+            case "2": return "FULL";
+        }
+        return value;
+    }
+
+    private void setJournalMode(String newValue) {
+        String value = executeForString("PRAGMA journal_mode", null, null);
+        if (!value.equalsIgnoreCase(newValue)) {
+            try {
+                String result = executeForString("PRAGMA journal_mode=" + newValue, null, null);
+                if (result.equalsIgnoreCase(newValue)) {
+                    return;
+                }
+                // PRAGMA journal_mode silently fails and returns the original journal
+                // mode in some cases if the journal mode could not be changed.
+            } catch (SQLiteDatabaseLockedException ex) {
+                // This error (SQLITE_BUSY) occurs if one connection has the database
+                // open in WAL mode and another tries to change it to non-WAL.
+            }
+            // Because we always disable WAL mode when a database is first opened
+            // (even if we intend to re-enable it), we can encounter problems if
+            // there is another open connection to the database somewhere.
+            // This can happen for a variety of reasons such as an application opening
+            // the same database in multiple processes at the same time or if there is a
+            // crashing content provider service that the ActivityManager has
+            // removed from its registry but whose process hasn't quite died yet
+            // by the time it is restarted in a new process.
+            //
+            // If we don't change the journal mode, nothing really bad happens.
+            // In the worst case, an application that enables WAL might not actually
+            // get it, although it can still use connection pooling.
+            Log.w(TAG, "Could not change the database journal mode of '"
+                    + mConfiguration.label + "' from '" + value + "' to '" + newValue
+                    + "' because the database is locked.  This usually means that "
+                    + "there are other open connections to the database which prevents "
+                    + "the database from enabling or disabling write-ahead logging mode.  "
+                    + "Proceeding without changing the journal mode.");
+        }
+    }
+
+    private void setLocaleFromConfiguration() {
+        if ((mConfiguration.openFlags & SQLiteDatabase.NO_LOCALIZED_COLLATORS) != 0) {
+            return;
+        }
+
+        // Register the localized collators.
+        final String newLocale = mConfiguration.locale.toString();
+        nativeRegisterLocalizedCollators(mConnectionPtr, newLocale);
+
+        if (!mConfiguration.isInMemoryDb()) {
+            checkDatabaseWiped();
+        }
+
+        // If the database is read-only, we cannot modify the android metadata table
+        // or existing indexes.
+        if (mIsReadOnlyConnection) {
+            return;
+        }
+
+        try {
+            // Ensure the android metadata table exists.
+            execute("CREATE TABLE IF NOT EXISTS android_metadata (locale TEXT)", null, null);
+
+            // Check whether the locale was actually changed.
+            final String oldLocale = executeForString("SELECT locale FROM android_metadata "
+                    + "UNION SELECT NULL ORDER BY locale DESC LIMIT 1", null, null);
+            if (oldLocale != null && oldLocale.equals(newLocale)) {
+                return;
+            }
+
+            // Go ahead and update the indexes using the new locale.
+            execute("BEGIN", null, null);
+            boolean success = false;
+            try {
+                execute("DELETE FROM android_metadata", null, null);
+                execute("INSERT INTO android_metadata (locale) VALUES(?)",
+                        new Object[] { newLocale }, null);
+                execute("REINDEX LOCALIZED", null, null);
+                success = true;
+            } finally {
+                execute(success ? "COMMIT" : "ROLLBACK", null, null);
+            }
+        } catch (SQLiteException ex) {
+            throw ex;
+        } catch (RuntimeException ex) {
+            throw new SQLiteException("Failed to change locale for db '" + mConfiguration.label
+                    + "' to '" + newLocale + "'.", ex);
+        }
+    }
+
+    private void setCustomFunctionsFromConfiguration() {
+        for (int i = 0; i < mConfiguration.customScalarFunctions.size(); i++) {
+            nativeRegisterCustomScalarFunction(mConnectionPtr,
+                    mConfiguration.customScalarFunctions.keyAt(i),
+                    mConfiguration.customScalarFunctions.valueAt(i));
+        }
+        for (int i = 0; i < mConfiguration.customAggregateFunctions.size(); i++) {
+            nativeRegisterCustomAggregateFunction(mConnectionPtr,
+                    mConfiguration.customAggregateFunctions.keyAt(i),
+                    mConfiguration.customAggregateFunctions.valueAt(i));
+        }
+    }
+
+    private void executePerConnectionSqlFromConfiguration(int startIndex) {
+        for (int i = startIndex; i < mConfiguration.perConnectionSql.size(); i++) {
+            final Pair<String, Object[]> statement = mConfiguration.perConnectionSql.get(i);
+            final int type = DatabaseUtils.getSqlStatementType(statement.first);
+            switch (type) {
+                case DatabaseUtils.STATEMENT_SELECT:
+                    executeForString(statement.first, statement.second, null);
+                    break;
+                case DatabaseUtils.STATEMENT_PRAGMA:
+                    execute(statement.first, statement.second, null);
+                    break;
+                default:
+                    throw new IllegalArgumentException(
+                            "Unsupported configuration statement: " + statement);
+            }
+        }
+    }
+
+    private void checkDatabaseWiped() {
+        if (!SQLiteGlobal.checkDbWipe()) {
+            return;
+        }
+        try {
+            final File checkFile = new File(mConfiguration.path
+                    + SQLiteGlobal.WIPE_CHECK_FILE_SUFFIX);
+
+            final boolean hasMetadataTable = executeForLong(
+                    "SELECT count(*) FROM sqlite_master"
+                            + " WHERE type='table' AND name='android_metadata'", null, null) > 0;
+            final boolean hasCheckFile = checkFile.exists();
+
+            if (!mIsReadOnlyConnection && !hasCheckFile) {
+                // Create the check file, unless it's a readonly connection,
+                // in which case we can't create the metadata table anyway.
+                checkFile.createNewFile();
+            }
+
+            if (!hasMetadataTable && hasCheckFile) {
+                // Bad. The DB is gone unexpectedly.
+                SQLiteDatabase.wipeDetected(mConfiguration.path, "unknown");
+            }
+
+        } catch (RuntimeException | IOException ex) {
+            SQLiteDatabase.wtfAsSystemServer(TAG,
+                    "Unexpected exception while checking for wipe", ex);
+        }
+    }
+
+    // Called by SQLiteConnectionPool only.
+    void reconfigure(SQLiteDatabaseConfiguration configuration) {
+        mOnlyAllowReadOnlyOperations = false;
+
+        // Remember what changed.
+        boolean foreignKeyModeChanged = configuration.foreignKeyConstraintsEnabled
+                != mConfiguration.foreignKeyConstraintsEnabled;
+        boolean walModeChanged = ((configuration.openFlags ^ mConfiguration.openFlags)
+                & (SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING
+                | SQLiteDatabase.ENABLE_LEGACY_COMPATIBILITY_WAL)) != 0;
+        boolean localeChanged = !configuration.locale.equals(mConfiguration.locale);
+        boolean customScalarFunctionsChanged = !configuration.customScalarFunctions
+                .equals(mConfiguration.customScalarFunctions);
+        boolean customAggregateFunctionsChanged = !configuration.customAggregateFunctions
+                .equals(mConfiguration.customAggregateFunctions);
+        final int oldSize = mConfiguration.perConnectionSql.size();
+        final int newSize = configuration.perConnectionSql.size();
+        boolean perConnectionSqlChanged = newSize > oldSize;
+
+        // Update configuration parameters.
+        mConfiguration.updateParametersFrom(configuration);
+
+        // Update prepared statement cache size.
+        mPreparedStatementCache.resize(configuration.maxSqlCacheSize);
+
+        if (foreignKeyModeChanged) {
+            setForeignKeyModeFromConfiguration();
+        }
+        if (walModeChanged) {
+            setWalModeFromConfiguration();
+        }
+        if (localeChanged) {
+            setLocaleFromConfiguration();
+        }
+        if (customScalarFunctionsChanged || customAggregateFunctionsChanged) {
+            setCustomFunctionsFromConfiguration();
+        }
+        if (perConnectionSqlChanged) {
+            executePerConnectionSqlFromConfiguration(oldSize);
+        }
+    }
+
+    // Called by SQLiteConnectionPool only.
+    // When set to true, executing write operations will throw SQLiteException.
+    // Preparing statements that might write is ok, just don't execute them.
+    void setOnlyAllowReadOnlyOperations(boolean readOnly) {
+        mOnlyAllowReadOnlyOperations = readOnly;
+    }
+
+    // Called by SQLiteConnectionPool only.
+    // Returns true if the prepared statement cache contains the specified SQL.
+    boolean isPreparedStatementInCache(String sql) {
+        return mPreparedStatementCache.get(sql) != null;
+    }
+
+    /**
+     * Gets the unique id of this connection.
+     * @return The connection id.
+     */
+    public int getConnectionId() {
+        return mConnectionId;
+    }
+
+    /**
+     * Returns true if this is the primary database connection.
+     * @return True if this is the primary database connection.
+     */
+    public boolean isPrimaryConnection() {
+        return mIsPrimaryConnection;
+    }
+
+    /**
+     * Prepares a statement for execution but does not bind its parameters or execute it.
+     * <p>
+     * This method can be used to check for syntax errors during compilation
+     * prior to execution of the statement.  If the {@code outStatementInfo} argument
+     * is not null, the provided {@link SQLiteStatementInfo} object is populated
+     * with information about the statement.
+     * </p><p>
+     * A prepared statement makes no reference to the arguments that may eventually
+     * be bound to it, consequently it it possible to cache certain prepared statements
+     * such as SELECT or INSERT/UPDATE statements.  If the statement is cacheable,
+     * then it will be stored in the cache for later.
+     * </p><p>
+     * To take advantage of this behavior as an optimization, the connection pool
+     * provides a method to acquire a connection that already has a given SQL statement
+     * in its prepared statement cache so that it is ready for execution.
+     * </p>
+     *
+     * @param sql The SQL statement to prepare.
+     * @param outStatementInfo The {@link SQLiteStatementInfo} object to populate
+     * with information about the statement, or null if none.
+     *
+     * @throws SQLiteException if an error occurs, such as a syntax error.
+     */
+    public void prepare(String sql, SQLiteStatementInfo outStatementInfo) {
+        if (sql == null) {
+            throw new IllegalArgumentException("sql must not be null.");
+        }
+
+        final int cookie = mRecentOperations.beginOperation("prepare", sql, null);
+        try {
+            final PreparedStatement statement = acquirePreparedStatement(sql);
+            try {
+                if (outStatementInfo != null) {
+                    outStatementInfo.numParameters = statement.mNumParameters;
+                    outStatementInfo.readOnly = statement.mReadOnly;
+
+                    final int columnCount = nativeGetColumnCount(
+                            mConnectionPtr, statement.mStatementPtr);
+                    if (columnCount == 0) {
+                        outStatementInfo.columnNames = EMPTY_STRING_ARRAY;
+                    } else {
+                        outStatementInfo.columnNames = new String[columnCount];
+                        for (int i = 0; i < columnCount; i++) {
+                            outStatementInfo.columnNames[i] = nativeGetColumnName(
+                                    mConnectionPtr, statement.mStatementPtr, i);
+                        }
+                    }
+                }
+            } finally {
+                releasePreparedStatement(statement);
+            }
+        } catch (RuntimeException ex) {
+            mRecentOperations.failOperation(cookie, ex);
+            throw ex;
+        } finally {
+            mRecentOperations.endOperation(cookie);
+        }
+    }
+
+    /**
+     * Executes a statement that does not return a result.
+     *
+     * @param sql The SQL statement to execute.
+     * @param bindArgs The arguments to bind, or null if none.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     *
+     * @throws SQLiteException if an error occurs, such as a syntax error
+     * or invalid number of bind arguments.
+     * @throws OperationCanceledException if the operation was canceled.
+     */
+    public void execute(String sql, Object[] bindArgs,
+            CancellationSignal cancellationSignal) {
+        if (sql == null) {
+            throw new IllegalArgumentException("sql must not be null.");
+        }
+
+        final int cookie = mRecentOperations.beginOperation("execute", sql, bindArgs);
+        try {
+            final PreparedStatement statement = acquirePreparedStatement(sql);
+            try {
+                throwIfStatementForbidden(statement);
+                bindArguments(statement, bindArgs);
+                applyBlockGuardPolicy(statement);
+                attachCancellationSignal(cancellationSignal);
+                try {
+                    nativeExecute(mConnectionPtr, statement.mStatementPtr);
+                } finally {
+                    detachCancellationSignal(cancellationSignal);
+                }
+            } finally {
+                releasePreparedStatement(statement);
+            }
+        } catch (RuntimeException ex) {
+            mRecentOperations.failOperation(cookie, ex);
+            throw ex;
+        } finally {
+            mRecentOperations.endOperation(cookie);
+        }
+    }
+
+    /**
+     * Executes a statement that returns a single <code>long</code> result.
+     *
+     * @param sql The SQL statement to execute.
+     * @param bindArgs The arguments to bind, or null if none.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * @return The value of the first column in the first row of the result set
+     * as a <code>long</code>, or zero if none.
+     *
+     * @throws SQLiteException if an error occurs, such as a syntax error
+     * or invalid number of bind arguments.
+     * @throws OperationCanceledException if the operation was canceled.
+     */
+    public long executeForLong(String sql, Object[] bindArgs,
+            CancellationSignal cancellationSignal) {
+        if (sql == null) {
+            throw new IllegalArgumentException("sql must not be null.");
+        }
+
+        final int cookie = mRecentOperations.beginOperation("executeForLong", sql, bindArgs);
+        try {
+            final PreparedStatement statement = acquirePreparedStatement(sql);
+            try {
+                throwIfStatementForbidden(statement);
+                bindArguments(statement, bindArgs);
+                applyBlockGuardPolicy(statement);
+                attachCancellationSignal(cancellationSignal);
+                try {
+                    long ret = nativeExecuteForLong(mConnectionPtr, statement.mStatementPtr);
+                    mRecentOperations.setResult(ret);
+                    return ret;
+                } finally {
+                    detachCancellationSignal(cancellationSignal);
+                }
+            } finally {
+                releasePreparedStatement(statement);
+            }
+        } catch (RuntimeException ex) {
+            mRecentOperations.failOperation(cookie, ex);
+            throw ex;
+        } finally {
+            mRecentOperations.endOperation(cookie);
+        }
+    }
+
+    /**
+     * Executes a statement that returns a single {@link String} result.
+     *
+     * @param sql The SQL statement to execute.
+     * @param bindArgs The arguments to bind, or null if none.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * @return The value of the first column in the first row of the result set
+     * as a <code>String</code>, or null if none.
+     *
+     * @throws SQLiteException if an error occurs, such as a syntax error
+     * or invalid number of bind arguments.
+     * @throws OperationCanceledException if the operation was canceled.
+     */
+    public String executeForString(String sql, Object[] bindArgs,
+            CancellationSignal cancellationSignal) {
+        if (sql == null) {
+            throw new IllegalArgumentException("sql must not be null.");
+        }
+
+        final int cookie = mRecentOperations.beginOperation("executeForString", sql, bindArgs);
+        try {
+            final PreparedStatement statement = acquirePreparedStatement(sql);
+            try {
+                throwIfStatementForbidden(statement);
+                bindArguments(statement, bindArgs);
+                applyBlockGuardPolicy(statement);
+                attachCancellationSignal(cancellationSignal);
+                try {
+                    String ret = nativeExecuteForString(mConnectionPtr, statement.mStatementPtr);
+                    mRecentOperations.setResult(ret);
+                    return ret;
+                } finally {
+                    detachCancellationSignal(cancellationSignal);
+                }
+            } finally {
+                releasePreparedStatement(statement);
+            }
+        } catch (RuntimeException ex) {
+            mRecentOperations.failOperation(cookie, ex);
+            throw ex;
+        } finally {
+            mRecentOperations.endOperation(cookie);
+        }
+    }
+
+    /**
+     * Executes a statement that returns a single BLOB result as a
+     * file descriptor to a shared memory region.
+     *
+     * @param sql The SQL statement to execute.
+     * @param bindArgs The arguments to bind, or null if none.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * @return The file descriptor for a shared memory region that contains
+     * the value of the first column in the first row of the result set as a BLOB,
+     * or null if none.
+     *
+     * @throws SQLiteException if an error occurs, such as a syntax error
+     * or invalid number of bind arguments.
+     * @throws OperationCanceledException if the operation was canceled.
+     */
+    public ParcelFileDescriptor executeForBlobFileDescriptor(String sql, Object[] bindArgs,
+            CancellationSignal cancellationSignal) {
+        if (sql == null) {
+            throw new IllegalArgumentException("sql must not be null.");
+        }
+
+        final int cookie = mRecentOperations.beginOperation("executeForBlobFileDescriptor",
+                sql, bindArgs);
+        try {
+            final PreparedStatement statement = acquirePreparedStatement(sql);
+            try {
+                throwIfStatementForbidden(statement);
+                bindArguments(statement, bindArgs);
+                applyBlockGuardPolicy(statement);
+                attachCancellationSignal(cancellationSignal);
+                try {
+                    int fd = nativeExecuteForBlobFileDescriptor(
+                            mConnectionPtr, statement.mStatementPtr);
+                    return fd >= 0 ? ParcelFileDescriptor.adoptFd(fd) : null;
+                } finally {
+                    detachCancellationSignal(cancellationSignal);
+                }
+            } finally {
+                releasePreparedStatement(statement);
+            }
+        } catch (RuntimeException ex) {
+            mRecentOperations.failOperation(cookie, ex);
+            throw ex;
+        } finally {
+            mRecentOperations.endOperation(cookie);
+        }
+    }
+
+    /**
+     * Executes a statement that returns a count of the number of rows
+     * that were changed.  Use for UPDATE or DELETE SQL statements.
+     *
+     * @param sql The SQL statement to execute.
+     * @param bindArgs The arguments to bind, or null if none.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * @return The number of rows that were changed.
+     *
+     * @throws SQLiteException if an error occurs, such as a syntax error
+     * or invalid number of bind arguments.
+     * @throws OperationCanceledException if the operation was canceled.
+     */
+    public int executeForChangedRowCount(String sql, Object[] bindArgs,
+            CancellationSignal cancellationSignal) {
+        if (sql == null) {
+            throw new IllegalArgumentException("sql must not be null.");
+        }
+
+        int changedRows = 0;
+        final int cookie = mRecentOperations.beginOperation("executeForChangedRowCount",
+                sql, bindArgs);
+        try {
+            final PreparedStatement statement = acquirePreparedStatement(sql);
+            try {
+                throwIfStatementForbidden(statement);
+                bindArguments(statement, bindArgs);
+                applyBlockGuardPolicy(statement);
+                attachCancellationSignal(cancellationSignal);
+                try {
+                    changedRows = nativeExecuteForChangedRowCount(
+                            mConnectionPtr, statement.mStatementPtr);
+                    return changedRows;
+                } finally {
+                    detachCancellationSignal(cancellationSignal);
+                }
+            } finally {
+                releasePreparedStatement(statement);
+            }
+        } catch (RuntimeException ex) {
+            mRecentOperations.failOperation(cookie, ex);
+            throw ex;
+        } finally {
+            if (mRecentOperations.endOperationDeferLog(cookie)) {
+                mRecentOperations.logOperation(cookie, "changedRows=" + changedRows);
+            }
+        }
+    }
+
+    /**
+     * Executes a statement that returns the row id of the last row inserted
+     * by the statement.  Use for INSERT SQL statements.
+     *
+     * @param sql The SQL statement to execute.
+     * @param bindArgs The arguments to bind, or null if none.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * @return The row id of the last row that was inserted, or 0 if none.
+     *
+     * @throws SQLiteException if an error occurs, such as a syntax error
+     * or invalid number of bind arguments.
+     * @throws OperationCanceledException if the operation was canceled.
+     */
+    public long executeForLastInsertedRowId(String sql, Object[] bindArgs,
+            CancellationSignal cancellationSignal) {
+        if (sql == null) {
+            throw new IllegalArgumentException("sql must not be null.");
+        }
+
+        final int cookie = mRecentOperations.beginOperation("executeForLastInsertedRowId",
+                sql, bindArgs);
+        try {
+            final PreparedStatement statement = acquirePreparedStatement(sql);
+            try {
+                throwIfStatementForbidden(statement);
+                bindArguments(statement, bindArgs);
+                applyBlockGuardPolicy(statement);
+                attachCancellationSignal(cancellationSignal);
+                try {
+                    return nativeExecuteForLastInsertedRowId(
+                            mConnectionPtr, statement.mStatementPtr);
+                } finally {
+                    detachCancellationSignal(cancellationSignal);
+                }
+            } finally {
+                releasePreparedStatement(statement);
+            }
+        } catch (RuntimeException ex) {
+            mRecentOperations.failOperation(cookie, ex);
+            throw ex;
+        } finally {
+            mRecentOperations.endOperation(cookie);
+        }
+    }
+
+    /**
+     * Executes a statement and populates the specified {@link CursorWindow}
+     * with a range of results.  Returns the number of rows that were counted
+     * during query execution.
+     *
+     * @param sql The SQL statement to execute.
+     * @param bindArgs The arguments to bind, or null if none.
+     * @param window The cursor window to clear and fill.
+     * @param startPos The start position for filling the window.
+     * @param requiredPos The position of a row that MUST be in the window.
+     * If it won't fit, then the query should discard part of what it filled
+     * so that it does.  Must be greater than or equal to <code>startPos</code>.
+     * @param countAllRows True to count all rows that the query would return
+     * regagless of whether they fit in the window.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * @return The number of rows that were counted during query execution.  Might
+     * not be all rows in the result set unless <code>countAllRows</code> is true.
+     *
+     * @throws SQLiteException if an error occurs, such as a syntax error
+     * or invalid number of bind arguments.
+     * @throws OperationCanceledException if the operation was canceled.
+     */
+    public int executeForCursorWindow(String sql, Object[] bindArgs,
+            CursorWindow window, int startPos, int requiredPos, boolean countAllRows,
+            CancellationSignal cancellationSignal) {
+        if (sql == null) {
+            throw new IllegalArgumentException("sql must not be null.");
+        }
+        if (window == null) {
+            throw new IllegalArgumentException("window must not be null.");
+        }
+
+        window.acquireReference();
+        try {
+            int actualPos = -1;
+            int countedRows = -1;
+            int filledRows = -1;
+            final int cookie = mRecentOperations.beginOperation("executeForCursorWindow",
+                    sql, bindArgs);
+            try {
+                final PreparedStatement statement = acquirePreparedStatement(sql);
+                try {
+                    throwIfStatementForbidden(statement);
+                    bindArguments(statement, bindArgs);
+                    applyBlockGuardPolicy(statement);
+                    attachCancellationSignal(cancellationSignal);
+                    try {
+                        final long result = nativeExecuteForCursorWindow(
+                                mConnectionPtr, statement.mStatementPtr, window.mWindowPtr,
+                                startPos, requiredPos, countAllRows);
+                        actualPos = (int)(result >> 32);
+                        countedRows = (int)result;
+                        filledRows = window.getNumRows();
+                        window.setStartPosition(actualPos);
+                        return countedRows;
+                    } finally {
+                        detachCancellationSignal(cancellationSignal);
+                    }
+                } finally {
+                    releasePreparedStatement(statement);
+                }
+            } catch (RuntimeException ex) {
+                mRecentOperations.failOperation(cookie, ex);
+                throw ex;
+            } finally {
+                if (mRecentOperations.endOperationDeferLog(cookie)) {
+                    mRecentOperations.logOperation(cookie, "window='" + window
+                            + "', startPos=" + startPos
+                            + ", actualPos=" + actualPos
+                            + ", filledRows=" + filledRows
+                            + ", countedRows=" + countedRows);
+                }
+            }
+        } finally {
+            window.releaseReference();
+        }
+    }
+
+    private PreparedStatement acquirePreparedStatement(String sql) {
+        PreparedStatement statement = mPreparedStatementCache.get(sql);
+        boolean skipCache = false;
+        if (statement != null) {
+            if (!statement.mInUse) {
+                return statement;
+            }
+            // The statement is already in the cache but is in use (this statement appears
+            // to be not only re-entrant but recursive!).  So prepare a new copy of the
+            // statement but do not cache it.
+            skipCache = true;
+        }
+
+        final long statementPtr = nativePrepareStatement(mConnectionPtr, sql);
+        try {
+            final int numParameters = nativeGetParameterCount(mConnectionPtr, statementPtr);
+            final int type = DatabaseUtils.getSqlStatementType(sql);
+            final boolean readOnly = nativeIsReadOnly(mConnectionPtr, statementPtr);
+            statement = obtainPreparedStatement(sql, statementPtr, numParameters, type, readOnly);
+            if (!skipCache && isCacheable(type)) {
+                mPreparedStatementCache.put(sql, statement);
+                statement.mInCache = true;
+            }
+        } catch (RuntimeException ex) {
+            // Finalize the statement if an exception occurred and we did not add
+            // it to the cache.  If it is already in the cache, then leave it there.
+            if (statement == null || !statement.mInCache) {
+                nativeFinalizeStatement(mConnectionPtr, statementPtr);
+            }
+            throw ex;
+        }
+        statement.mInUse = true;
+        return statement;
+    }
+
+    private void releasePreparedStatement(PreparedStatement statement) {
+        statement.mInUse = false;
+        if (statement.mInCache) {
+            try {
+                nativeResetStatementAndClearBindings(mConnectionPtr, statement.mStatementPtr);
+            } catch (SQLiteException ex) {
+                // The statement could not be reset due to an error.  Remove it from the cache.
+                // When remove() is called, the cache will invoke its entryRemoved() callback,
+                // which will in turn call finalizePreparedStatement() to finalize and
+                // recycle the statement.
+                if (DEBUG) {
+                    Log.d(TAG, "Could not reset prepared statement due to an exception.  "
+                            + "Removing it from the cache.  SQL: "
+                            + trimSqlForDisplay(statement.mSql), ex);
+                }
+
+                mPreparedStatementCache.remove(statement.mSql);
+            }
+        } else {
+            finalizePreparedStatement(statement);
+        }
+    }
+
+    private void finalizePreparedStatement(PreparedStatement statement) {
+        nativeFinalizeStatement(mConnectionPtr, statement.mStatementPtr);
+        recyclePreparedStatement(statement);
+    }
+
+    private void attachCancellationSignal(CancellationSignal cancellationSignal) {
+        if (cancellationSignal != null) {
+            cancellationSignal.throwIfCanceled();
+
+            mCancellationSignalAttachCount += 1;
+            if (mCancellationSignalAttachCount == 1) {
+                // Reset cancellation flag before executing the statement.
+                nativeResetCancel(mConnectionPtr, true /*cancelable*/);
+
+                // After this point, onCancel() may be called concurrently.
+                cancellationSignal.setOnCancelListener(this);
+            }
+        }
+    }
+
+    private void detachCancellationSignal(CancellationSignal cancellationSignal) {
+        if (cancellationSignal != null) {
+            assert mCancellationSignalAttachCount > 0;
+
+            mCancellationSignalAttachCount -= 1;
+            if (mCancellationSignalAttachCount == 0) {
+                // After this point, onCancel() cannot be called concurrently.
+                cancellationSignal.setOnCancelListener(null);
+
+                // Reset cancellation flag after executing the statement.
+                nativeResetCancel(mConnectionPtr, false /*cancelable*/);
+            }
+        }
+    }
+
+    // CancellationSignal.OnCancelListener callback.
+    // This method may be called on a different thread than the executing statement.
+    // However, it will only be called between calls to attachCancellationSignal and
+    // detachCancellationSignal, while a statement is executing.  We can safely assume
+    // that the SQLite connection is still alive.
+    @Override
+    public void onCancel() {
+        nativeCancel(mConnectionPtr);
+    }
+
+    private void bindArguments(PreparedStatement statement, Object[] bindArgs) {
+        final int count = bindArgs != null ? bindArgs.length : 0;
+        if (count != statement.mNumParameters) {
+            throw new SQLiteBindOrColumnIndexOutOfRangeException(
+                    "Expected " + statement.mNumParameters + " bind arguments but "
+                    + count + " were provided.");
+        }
+        if (count == 0) {
+            return;
+        }
+
+        final long statementPtr = statement.mStatementPtr;
+        for (int i = 0; i < count; i++) {
+            final Object arg = bindArgs[i];
+            switch (DatabaseUtils.getTypeOfObject(arg)) {
+                case Cursor.FIELD_TYPE_NULL:
+                    nativeBindNull(mConnectionPtr, statementPtr, i + 1);
+                    break;
+                case Cursor.FIELD_TYPE_INTEGER:
+                    nativeBindLong(mConnectionPtr, statementPtr, i + 1,
+                            ((Number)arg).longValue());
+                    break;
+                case Cursor.FIELD_TYPE_FLOAT:
+                    nativeBindDouble(mConnectionPtr, statementPtr, i + 1,
+                            ((Number)arg).doubleValue());
+                    break;
+                case Cursor.FIELD_TYPE_BLOB:
+                    nativeBindBlob(mConnectionPtr, statementPtr, i + 1, (byte[])arg);
+                    break;
+                case Cursor.FIELD_TYPE_STRING:
+                default:
+                    if (arg instanceof Boolean) {
+                        // Provide compatibility with legacy applications which may pass
+                        // Boolean values in bind args.
+                        nativeBindLong(mConnectionPtr, statementPtr, i + 1,
+                                ((Boolean)arg).booleanValue() ? 1 : 0);
+                    } else {
+                        nativeBindString(mConnectionPtr, statementPtr, i + 1, arg.toString());
+                    }
+                    break;
+            }
+        }
+    }
+
+    private void throwIfStatementForbidden(PreparedStatement statement) {
+        if (mOnlyAllowReadOnlyOperations && !statement.mReadOnly) {
+            throw new SQLiteException("Cannot execute this statement because it "
+                    + "might modify the database but the connection is read-only.");
+        }
+    }
+
+    private static boolean isCacheable(int statementType) {
+        if (statementType == DatabaseUtils.STATEMENT_UPDATE
+                || statementType == DatabaseUtils.STATEMENT_SELECT) {
+            return true;
+        }
+        return false;
+    }
+
+    private void applyBlockGuardPolicy(PreparedStatement statement) {
+        if (!mConfiguration.isInMemoryDb()) {
+            if (statement.mReadOnly) {
+                BlockGuard.getThreadPolicy().onReadFromDisk();
+            } else {
+                BlockGuard.getThreadPolicy().onWriteToDisk();
+            }
+        }
+    }
+
+    /**
+     * Dumps debugging information about this connection.
+     *
+     * @param printer The printer to receive the dump, not null.
+     * @param verbose True to dump more verbose information.
+     */
+    public void dump(Printer printer, boolean verbose) {
+        dumpUnsafe(printer, verbose);
+    }
+
+    /**
+     * Dumps debugging information about this connection, in the case where the
+     * caller might not actually own the connection.
+     *
+     * This function is written so that it may be called by a thread that does not
+     * own the connection.  We need to be very careful because the connection state is
+     * not synchronized.
+     *
+     * At worst, the method may return stale or slightly wrong data, however
+     * it should not crash.  This is ok as it is only used for diagnostic purposes.
+     *
+     * @param printer The printer to receive the dump, not null.
+     * @param verbose True to dump more verbose information.
+     */
+    void dumpUnsafe(Printer printer, boolean verbose) {
+        printer.println("Connection #" + mConnectionId + ":");
+        if (verbose) {
+            printer.println("  connectionPtr: 0x" + Long.toHexString(mConnectionPtr));
+        }
+        printer.println("  isPrimaryConnection: " + mIsPrimaryConnection);
+        printer.println("  onlyAllowReadOnlyOperations: " + mOnlyAllowReadOnlyOperations);
+
+        mRecentOperations.dump(printer);
+
+        if (verbose) {
+            mPreparedStatementCache.dump(printer);
+        }
+    }
+
+    /**
+     * Describes the currently executing operation, in the case where the
+     * caller might not actually own the connection.
+     *
+     * This function is written so that it may be called by a thread that does not
+     * own the connection.  We need to be very careful because the connection state is
+     * not synchronized.
+     *
+     * At worst, the method may return stale or slightly wrong data, however
+     * it should not crash.  This is ok as it is only used for diagnostic purposes.
+     *
+     * @return A description of the current operation including how long it has been running,
+     * or null if none.
+     */
+    String describeCurrentOperationUnsafe() {
+        return mRecentOperations.describeCurrentOperation();
+    }
+
+    /**
+     * Collects statistics about database connection memory usage.
+     *
+     * @param dbStatsList The list to populate.
+     */
+    void collectDbStats(ArrayList<DbStats> dbStatsList) {
+        // Get information about the main database.
+        int lookaside = nativeGetDbLookaside(mConnectionPtr);
+        long pageCount = 0;
+        long pageSize = 0;
+        try {
+            pageCount = executeForLong("PRAGMA page_count;", null, null);
+            pageSize = executeForLong("PRAGMA page_size;", null, null);
+        } catch (SQLiteException ex) {
+            // Ignore.
+        }
+        dbStatsList.add(getMainDbStatsUnsafe(lookaside, pageCount, pageSize));
+
+        // Get information about attached databases.
+        // We ignore the first row in the database list because it corresponds to
+        // the main database which we have already described.
+        CursorWindow window = new CursorWindow("collectDbStats");
+        try {
+            executeForCursorWindow("PRAGMA database_list;", null, window, 0, 0, false, null);
+            for (int i = 1; i < window.getNumRows(); i++) {
+                String name = window.getString(i, 1);
+                String path = window.getString(i, 2);
+                pageCount = 0;
+                pageSize = 0;
+                try {
+                    pageCount = executeForLong("PRAGMA " + name + ".page_count;", null, null);
+                    pageSize = executeForLong("PRAGMA " + name + ".page_size;", null, null);
+                } catch (SQLiteException ex) {
+                    // Ignore.
+                }
+                String label = "  (attached) " + name;
+                if (!path.isEmpty()) {
+                    label += ": " + path;
+                }
+                dbStatsList.add(new DbStats(label, pageCount, pageSize, 0, 0, 0, 0));
+            }
+        } catch (SQLiteException ex) {
+            // Ignore.
+        } finally {
+            window.close();
+        }
+    }
+
+    /**
+     * Collects statistics about database connection memory usage, in the case where the
+     * caller might not actually own the connection.
+     *
+     * @return The statistics object, never null.
+     */
+    void collectDbStatsUnsafe(ArrayList<DbStats> dbStatsList) {
+        dbStatsList.add(getMainDbStatsUnsafe(0, 0, 0));
+    }
+
+    private DbStats getMainDbStatsUnsafe(int lookaside, long pageCount, long pageSize) {
+        // The prepared statement cache is thread-safe so we can access its statistics
+        // even if we do not own the database connection.
+        String label = mConfiguration.path;
+        if (!mIsPrimaryConnection) {
+            label += " (" + mConnectionId + ")";
+        }
+        return new DbStats(label, pageCount, pageSize, lookaside,
+                mPreparedStatementCache.hitCount(),
+                mPreparedStatementCache.missCount(),
+                mPreparedStatementCache.size());
+    }
+
+    @Override
+    public String toString() {
+        return "SQLiteConnection: " + mConfiguration.path + " (" + mConnectionId + ")";
+    }
+
+    private PreparedStatement obtainPreparedStatement(String sql, long statementPtr,
+            int numParameters, int type, boolean readOnly) {
+        PreparedStatement statement = mPreparedStatementPool;
+        if (statement != null) {
+            mPreparedStatementPool = statement.mPoolNext;
+            statement.mPoolNext = null;
+            statement.mInCache = false;
+        } else {
+            statement = new PreparedStatement();
+        }
+        statement.mSql = sql;
+        statement.mStatementPtr = statementPtr;
+        statement.mNumParameters = numParameters;
+        statement.mType = type;
+        statement.mReadOnly = readOnly;
+        return statement;
+    }
+
+    private void recyclePreparedStatement(PreparedStatement statement) {
+        statement.mSql = null;
+        statement.mPoolNext = mPreparedStatementPool;
+        mPreparedStatementPool = statement;
+    }
+
+    private static String trimSqlForDisplay(String sql) {
+        // Note: Creating and caching a regular expression is expensive at preload-time
+        //       and stops compile-time initialization. This pattern is only used when
+        //       dumping the connection, which is a rare (mainly error) case. So:
+        //       DO NOT CACHE.
+        return sql.replaceAll("[\\s]*\\n+[\\s]*", " ");
+    }
+
+    /**
+     * Holder type for a prepared statement.
+     *
+     * Although this object holds a pointer to a native statement object, it
+     * does not have a finalizer.  This is deliberate.  The {@link SQLiteConnection}
+     * owns the statement object and will take care of freeing it when needed.
+     * In particular, closing the connection requires a guarantee of deterministic
+     * resource disposal because all native statement objects must be freed before
+     * the native database object can be closed.  So no finalizers here.
+     */
+    private static final class PreparedStatement {
+        // Next item in pool.
+        public PreparedStatement mPoolNext;
+
+        // The SQL from which the statement was prepared.
+        public String mSql;
+
+        // The native sqlite3_stmt object pointer.
+        // Lifetime is managed explicitly by the connection.
+        public long mStatementPtr;
+
+        // The number of parameters that the prepared statement has.
+        public int mNumParameters;
+
+        // The statement type.
+        public int mType;
+
+        // True if the statement is read-only.
+        public boolean mReadOnly;
+
+        // True if the statement is in the cache.
+        public boolean mInCache;
+
+        // True if the statement is in use (currently executing).
+        // We need this flag because due to the use of custom functions in triggers, it's
+        // possible for SQLite calls to be re-entrant.  Consequently we need to prevent
+        // in use statements from being finalized until they are no longer in use.
+        public boolean mInUse;
+    }
+
+    private final class PreparedStatementCache
+            extends LruCache<String, PreparedStatement> {
+        public PreparedStatementCache(int size) {
+            super(size);
+        }
+
+        @Override
+        protected void entryRemoved(boolean evicted, String key,
+                PreparedStatement oldValue, PreparedStatement newValue) {
+            oldValue.mInCache = false;
+            if (!oldValue.mInUse) {
+                finalizePreparedStatement(oldValue);
+            }
+        }
+
+        public void dump(Printer printer) {
+            printer.println("  Prepared statement cache:");
+            Map<String, PreparedStatement> cache = snapshot();
+            if (!cache.isEmpty()) {
+                int i = 0;
+                for (Map.Entry<String, PreparedStatement> entry : cache.entrySet()) {
+                    PreparedStatement statement = entry.getValue();
+                    if (statement.mInCache) { // might be false due to a race with entryRemoved
+                        String sql = entry.getKey();
+                        printer.println("    " + i + ": statementPtr=0x"
+                                + Long.toHexString(statement.mStatementPtr)
+                                + ", numParameters=" + statement.mNumParameters
+                                + ", type=" + statement.mType
+                                + ", readOnly=" + statement.mReadOnly
+                                + ", sql=\"" + trimSqlForDisplay(sql) + "\"");
+                    }
+                    i += 1;
+                }
+            } else {
+                printer.println("    <none>");
+            }
+        }
+    }
+
+    private static final class OperationLog {
+        private static final int MAX_RECENT_OPERATIONS = 20;
+        private static final int COOKIE_GENERATION_SHIFT = 8;
+        private static final int COOKIE_INDEX_MASK = 0xff;
+
+        private final Operation[] mOperations = new Operation[MAX_RECENT_OPERATIONS];
+        private int mIndex;
+        private int mGeneration;
+        private final SQLiteConnectionPool mPool;
+        private long mResultLong = Long.MIN_VALUE;
+        private String mResultString;
+
+        OperationLog(SQLiteConnectionPool pool) {
+            mPool = pool;
+        }
+
+        public int beginOperation(String kind, String sql, Object[] bindArgs) {
+            mResultLong = Long.MIN_VALUE;
+            mResultString = null;
+
+            synchronized (mOperations) {
+                final int index = (mIndex + 1) % MAX_RECENT_OPERATIONS;
+                Operation operation = mOperations[index];
+                if (operation == null) {
+                    operation = new Operation();
+                    mOperations[index] = operation;
+                } else {
+                    operation.mFinished = false;
+                    operation.mException = null;
+                    if (operation.mBindArgs != null) {
+                        operation.mBindArgs.clear();
+                    }
+                }
+                operation.mStartWallTime = System.currentTimeMillis();
+                operation.mStartTime = SystemClock.uptimeMillis();
+                operation.mKind = kind;
+                operation.mSql = sql;
+                operation.mPath = mPool.getPath();
+                operation.mResultLong = Long.MIN_VALUE;
+                operation.mResultString = null;
+                if (bindArgs != null) {
+                    if (operation.mBindArgs == null) {
+                        operation.mBindArgs = new ArrayList<Object>();
+                    } else {
+                        operation.mBindArgs.clear();
+                    }
+                    for (int i = 0; i < bindArgs.length; i++) {
+                        final Object arg = bindArgs[i];
+                        if (arg != null && arg instanceof byte[]) {
+                            // Don't hold onto the real byte array longer than necessary.
+                            operation.mBindArgs.add(EMPTY_BYTE_ARRAY);
+                        } else {
+                            operation.mBindArgs.add(arg);
+                        }
+                    }
+                }
+                operation.mCookie = newOperationCookieLocked(index);
+                if (Trace.isTagEnabled(Trace.TRACE_TAG_DATABASE)) {
+                    Trace.asyncTraceBegin(Trace.TRACE_TAG_DATABASE, operation.getTraceMethodName(),
+                            operation.mCookie);
+                }
+                mIndex = index;
+                return operation.mCookie;
+            }
+        }
+
+        public void failOperation(int cookie, Exception ex) {
+            synchronized (mOperations) {
+                final Operation operation = getOperationLocked(cookie);
+                if (operation != null) {
+                    operation.mException = ex;
+                }
+            }
+        }
+
+        public void endOperation(int cookie) {
+            synchronized (mOperations) {
+                if (endOperationDeferLogLocked(cookie)) {
+                    logOperationLocked(cookie, null);
+                }
+            }
+        }
+
+        public boolean endOperationDeferLog(int cookie) {
+            synchronized (mOperations) {
+                return endOperationDeferLogLocked(cookie);
+            }
+        }
+
+        public void logOperation(int cookie, String detail) {
+            synchronized (mOperations) {
+                logOperationLocked(cookie, detail);
+            }
+        }
+
+        public void setResult(long longResult) {
+            mResultLong = longResult;
+        }
+
+        public void setResult(String stringResult) {
+            mResultString = stringResult;
+        }
+
+        private boolean endOperationDeferLogLocked(int cookie) {
+            final Operation operation = getOperationLocked(cookie);
+            if (operation != null) {
+                if (Trace.isTagEnabled(Trace.TRACE_TAG_DATABASE)) {
+                    Trace.asyncTraceEnd(Trace.TRACE_TAG_DATABASE, operation.getTraceMethodName(),
+                            operation.mCookie);
+                }
+                operation.mEndTime = SystemClock.uptimeMillis();
+                operation.mFinished = true;
+                final long execTime = operation.mEndTime - operation.mStartTime;
+                mPool.onStatementExecuted(execTime);
+                return NoPreloadHolder.DEBUG_LOG_SLOW_QUERIES && SQLiteDebug.shouldLogSlowQuery(
+                        execTime);
+            }
+            return false;
+        }
+
+        private void logOperationLocked(int cookie, String detail) {
+            final Operation operation = getOperationLocked(cookie);
+            operation.mResultLong = mResultLong;
+            operation.mResultString = mResultString;
+            StringBuilder msg = new StringBuilder();
+            operation.describe(msg, true);
+            if (detail != null) {
+                msg.append(", ").append(detail);
+            }
+            Log.d(TAG, msg.toString());
+        }
+
+        private int newOperationCookieLocked(int index) {
+            final int generation = mGeneration++;
+            return generation << COOKIE_GENERATION_SHIFT | index;
+        }
+
+        private Operation getOperationLocked(int cookie) {
+            final int index = cookie & COOKIE_INDEX_MASK;
+            final Operation operation = mOperations[index];
+            return operation.mCookie == cookie ? operation : null;
+        }
+
+        public String describeCurrentOperation() {
+            synchronized (mOperations) {
+                final Operation operation = mOperations[mIndex];
+                if (operation != null && !operation.mFinished) {
+                    StringBuilder msg = new StringBuilder();
+                    operation.describe(msg, false);
+                    return msg.toString();
+                }
+                return null;
+            }
+        }
+
+        public void dump(Printer printer) {
+            synchronized (mOperations) {
+                printer.println("  Most recently executed operations:");
+                int index = mIndex;
+                Operation operation = mOperations[index];
+                if (operation != null) {
+                    // Note: SimpleDateFormat is not thread-safe, cannot be compile-time created,
+                    // and is relatively expensive to create during preloading. This method is only
+                    // used when dumping a connection, which is a rare (mainly error) case.
+                    SimpleDateFormat opDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+                    int n = 0;
+                    do {
+                        StringBuilder msg = new StringBuilder();
+                        msg.append("    ").append(n).append(": [");
+                        String formattedStartTime = opDF.format(new Date(operation.mStartWallTime));
+                        msg.append(formattedStartTime);
+                        msg.append("] ");
+                        operation.describe(msg, false); // Never dump bingargs in a bugreport
+                        printer.println(msg.toString());
+
+                        if (index > 0) {
+                            index -= 1;
+                        } else {
+                            index = MAX_RECENT_OPERATIONS - 1;
+                        }
+                        n += 1;
+                        operation = mOperations[index];
+                    } while (operation != null && n < MAX_RECENT_OPERATIONS);
+                } else {
+                    printer.println("    <none>");
+                }
+            }
+        }
+    }
+
+    private static final class Operation {
+        // Trim all SQL statements to 256 characters inside the trace marker.
+        // This limit gives plenty of context while leaving space for other
+        // entries in the trace buffer (and ensures atrace doesn't truncate the
+        // marker for us, potentially losing metadata in the process).
+        private static final int MAX_TRACE_METHOD_NAME_LEN = 256;
+
+        public long mStartWallTime; // in System.currentTimeMillis()
+        public long mStartTime; // in SystemClock.uptimeMillis();
+        public long mEndTime; // in SystemClock.uptimeMillis();
+        public String mKind;
+        public String mSql;
+        public ArrayList<Object> mBindArgs;
+        public boolean mFinished;
+        public Exception mException;
+        public int mCookie;
+        public String mPath;
+        public long mResultLong; // MIN_VALUE means "value not set".
+        public String mResultString;
+
+        public void describe(StringBuilder msg, boolean allowDetailedLog) {
+            msg.append(mKind);
+            if (mFinished) {
+                msg.append(" took ").append(mEndTime - mStartTime).append("ms");
+            } else {
+                msg.append(" started ").append(System.currentTimeMillis() - mStartWallTime)
+                        .append("ms ago");
+            }
+            msg.append(" - ").append(getStatus());
+            if (mSql != null) {
+                msg.append(", sql=\"").append(trimSqlForDisplay(mSql)).append("\"");
+            }
+            final boolean dumpDetails = allowDetailedLog && NoPreloadHolder.DEBUG_LOG_DETAILED
+                    && mBindArgs != null && mBindArgs.size() != 0;
+            if (dumpDetails) {
+                msg.append(", bindArgs=[");
+                final int count = mBindArgs.size();
+                for (int i = 0; i < count; i++) {
+                    final Object arg = mBindArgs.get(i);
+                    if (i != 0) {
+                        msg.append(", ");
+                    }
+                    if (arg == null) {
+                        msg.append("null");
+                    } else if (arg instanceof byte[]) {
+                        msg.append("<byte[]>");
+                    } else if (arg instanceof String) {
+                        msg.append("\"").append((String)arg).append("\"");
+                    } else {
+                        msg.append(arg);
+                    }
+                }
+                msg.append("]");
+            }
+            msg.append(", path=").append(mPath);
+            if (mException != null) {
+                msg.append(", exception=\"").append(mException.getMessage()).append("\"");
+            }
+            if (mResultLong != Long.MIN_VALUE) {
+                msg.append(", result=").append(mResultLong);
+            }
+            if (mResultString != null) {
+                msg.append(", result=\"").append(mResultString).append("\"");
+            }
+        }
+
+        private String getStatus() {
+            if (!mFinished) {
+                return "running";
+            }
+            return mException != null ? "failed" : "succeeded";
+        }
+
+        private String getTraceMethodName() {
+            String methodName = mKind + " " + mSql;
+            if (methodName.length() > MAX_TRACE_METHOD_NAME_LEN)
+                return methodName.substring(0, MAX_TRACE_METHOD_NAME_LEN);
+            return methodName;
+        }
+
+    }
+}
diff --git a/android/database/sqlite/SQLiteConnectionPool.java b/android/database/sqlite/SQLiteConnectionPool.java
new file mode 100644
index 0000000..852f8f2
--- /dev/null
+++ b/android/database/sqlite/SQLiteConnectionPool.java
@@ -0,0 +1,1244 @@
+/*
+ * Copyright (C) 2011 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 android.database.sqlite;
+
+import android.database.sqlite.SQLiteDebug.DbStats;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.OperationCanceledException;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.PrefixPrinter;
+import android.util.Printer;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import dalvik.system.CloseGuard;
+
+import java.io.Closeable;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.LockSupport;
+
+/**
+ * Maintains a pool of active SQLite database connections.
+ * <p>
+ * At any given time, a connection is either owned by the pool, or it has been
+ * acquired by a {@link SQLiteSession}.  When the {@link SQLiteSession} is
+ * finished with the connection it is using, it must return the connection
+ * back to the pool.
+ * </p><p>
+ * The pool holds strong references to the connections it owns.  However,
+ * it only holds <em>weak references</em> to the connections that sessions
+ * have acquired from it.  Using weak references in the latter case ensures
+ * that the connection pool can detect when connections have been improperly
+ * abandoned so that it can create new connections to replace them if needed.
+ * </p><p>
+ * The connection pool is thread-safe (but the connections themselves are not).
+ * </p>
+ *
+ * <h2>Exception safety</h2>
+ * <p>
+ * This code attempts to maintain the invariant that opened connections are
+ * always owned.  Unfortunately that means it needs to handle exceptions
+ * all over to ensure that broken connections get cleaned up.  Most
+ * operations invokving SQLite can throw {@link SQLiteException} or other
+ * runtime exceptions.  This is a bit of a pain to deal with because the compiler
+ * cannot help us catch missing exception handling code.
+ * </p><p>
+ * The general rule for this file: If we are making calls out to
+ * {@link SQLiteConnection} then we must be prepared to handle any
+ * runtime exceptions it might throw at us.  Note that out-of-memory
+ * is an {@link Error}, not a {@link RuntimeException}.  We don't trouble ourselves
+ * handling out of memory because it is hard to do anything at all sensible then
+ * and most likely the VM is about to crash.
+ * </p>
+ *
+ * @hide
+ */
+public final class SQLiteConnectionPool implements Closeable {
+    private static final String TAG = "SQLiteConnectionPool";
+
+    // Amount of time to wait in milliseconds before unblocking acquireConnection
+    // and logging a message about the connection pool being busy.
+    private static final long CONNECTION_POOL_BUSY_MILLIS = 30 * 1000; // 30 seconds
+
+    private final CloseGuard mCloseGuard = CloseGuard.get();
+
+    private final Object mLock = new Object();
+    private final AtomicBoolean mConnectionLeaked = new AtomicBoolean();
+    private final SQLiteDatabaseConfiguration mConfiguration;
+    private int mMaxConnectionPoolSize;
+    private boolean mIsOpen;
+    private int mNextConnectionId;
+
+    private ConnectionWaiter mConnectionWaiterPool;
+    private ConnectionWaiter mConnectionWaiterQueue;
+
+    // Strong references to all available connections.
+    private final ArrayList<SQLiteConnection> mAvailableNonPrimaryConnections =
+            new ArrayList<SQLiteConnection>();
+    private SQLiteConnection mAvailablePrimaryConnection;
+
+    @GuardedBy("mLock")
+    private IdleConnectionHandler mIdleConnectionHandler;
+
+    private final AtomicLong mTotalExecutionTimeCounter = new AtomicLong(0);
+
+    // Describes what should happen to an acquired connection when it is returned to the pool.
+    enum AcquiredConnectionStatus {
+        // The connection should be returned to the pool as usual.
+        NORMAL,
+
+        // The connection must be reconfigured before being returned.
+        RECONFIGURE,
+
+        // The connection must be closed and discarded.
+        DISCARD,
+    }
+
+    // Weak references to all acquired connections.  The associated value
+    // indicates whether the connection must be reconfigured before being
+    // returned to the available connection list or discarded.
+    // For example, the prepared statement cache size may have changed and
+    // need to be updated in preparation for the next client.
+    private final WeakHashMap<SQLiteConnection, AcquiredConnectionStatus> mAcquiredConnections =
+            new WeakHashMap<SQLiteConnection, AcquiredConnectionStatus>();
+
+    /**
+     * Connection flag: Read-only.
+     * <p>
+     * This flag indicates that the connection will only be used to
+     * perform read-only operations.
+     * </p>
+     */
+    public static final int CONNECTION_FLAG_READ_ONLY = 1 << 0;
+
+    /**
+     * Connection flag: Primary connection affinity.
+     * <p>
+     * This flag indicates that the primary connection is required.
+     * This flag helps support legacy applications that expect most data modifying
+     * operations to be serialized by locking the primary database connection.
+     * Setting this flag essentially implements the old "db lock" concept by preventing
+     * an operation from being performed until it can obtain exclusive access to
+     * the primary connection.
+     * </p>
+     */
+    public static final int CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY = 1 << 1;
+
+    /**
+     * Connection flag: Connection is being used interactively.
+     * <p>
+     * This flag indicates that the connection is needed by the UI thread.
+     * The connection pool can use this flag to elevate the priority
+     * of the database connection request.
+     * </p>
+     */
+    public static final int CONNECTION_FLAG_INTERACTIVE = 1 << 2;
+
+    private SQLiteConnectionPool(SQLiteDatabaseConfiguration configuration) {
+        mConfiguration = new SQLiteDatabaseConfiguration(configuration);
+        setMaxConnectionPoolSizeLocked();
+        // If timeout is set, setup idle connection handler
+        // In case of MAX_VALUE - idle connections are never closed
+        if (mConfiguration.idleConnectionTimeoutMs != Long.MAX_VALUE) {
+            setupIdleConnectionHandler(Looper.getMainLooper(),
+                    mConfiguration.idleConnectionTimeoutMs);
+        }
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            dispose(true);
+        } finally {
+            super.finalize();
+        }
+    }
+
+    /**
+     * Opens a connection pool for the specified database.
+     *
+     * @param configuration The database configuration.
+     * @return The connection pool.
+     *
+     * @throws SQLiteException if a database error occurs.
+     */
+    public static SQLiteConnectionPool open(SQLiteDatabaseConfiguration configuration) {
+        if (configuration == null) {
+            throw new IllegalArgumentException("configuration must not be null.");
+        }
+
+        // Create the pool.
+        SQLiteConnectionPool pool = new SQLiteConnectionPool(configuration);
+        pool.open(); // might throw
+        return pool;
+    }
+
+    // Might throw
+    private void open() {
+        // Open the primary connection.
+        // This might throw if the database is corrupt.
+        mAvailablePrimaryConnection = openConnectionLocked(mConfiguration,
+                true /*primaryConnection*/); // might throw
+        // Mark it released so it can be closed after idle timeout
+        synchronized (mLock) {
+            if (mIdleConnectionHandler != null) {
+                mIdleConnectionHandler.connectionReleased(mAvailablePrimaryConnection);
+            }
+        }
+
+        // Mark the pool as being open for business.
+        mIsOpen = true;
+        mCloseGuard.open("close");
+    }
+
+    /**
+     * Closes the connection pool.
+     * <p>
+     * When the connection pool is closed, it will refuse all further requests
+     * to acquire connections.  All connections that are currently available in
+     * the pool are closed immediately.  Any connections that are still in use
+     * will be closed as soon as they are returned to the pool.
+     * </p>
+     *
+     * @throws IllegalStateException if the pool has been closed.
+     */
+    public void close() {
+        dispose(false);
+    }
+
+    private void dispose(boolean finalized) {
+        if (mCloseGuard != null) {
+            if (finalized) {
+                mCloseGuard.warnIfOpen();
+            }
+            mCloseGuard.close();
+        }
+
+        if (!finalized) {
+            // Close all connections.  We don't need (or want) to do this
+            // when finalized because we don't know what state the connections
+            // themselves will be in.  The finalizer is really just here for CloseGuard.
+            // The connections will take care of themselves when their own finalizers run.
+            synchronized (mLock) {
+                throwIfClosedLocked();
+
+                mIsOpen = false;
+
+                closeAvailableConnectionsAndLogExceptionsLocked();
+
+                final int pendingCount = mAcquiredConnections.size();
+                if (pendingCount != 0) {
+                    Log.i(TAG, "The connection pool for " + mConfiguration.label
+                            + " has been closed but there are still "
+                            + pendingCount + " connections in use.  They will be closed "
+                            + "as they are released back to the pool.");
+                }
+
+                wakeConnectionWaitersLocked();
+            }
+        }
+    }
+
+    /**
+     * Reconfigures the database configuration of the connection pool and all of its
+     * connections.
+     * <p>
+     * Configuration changes are propagated down to connections immediately if
+     * they are available or as soon as they are released.  This includes changes
+     * that affect the size of the pool.
+     * </p>
+     *
+     * @param configuration The new configuration.
+     *
+     * @throws IllegalStateException if the pool has been closed.
+     */
+    public void reconfigure(SQLiteDatabaseConfiguration configuration) {
+        if (configuration == null) {
+            throw new IllegalArgumentException("configuration must not be null.");
+        }
+
+        synchronized (mLock) {
+            throwIfClosedLocked();
+
+            boolean walModeChanged = ((configuration.openFlags ^ mConfiguration.openFlags)
+                    & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0;
+            if (walModeChanged) {
+                // WAL mode can only be changed if there are no acquired connections
+                // because we need to close all but the primary connection first.
+                if (!mAcquiredConnections.isEmpty()) {
+                    throw new IllegalStateException("Write Ahead Logging (WAL) mode cannot "
+                            + "be enabled or disabled while there are transactions in "
+                            + "progress.  Finish all transactions and release all active "
+                            + "database connections first.");
+                }
+
+                // Close all non-primary connections.  This should happen immediately
+                // because none of them are in use.
+                closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked();
+                assert mAvailableNonPrimaryConnections.isEmpty();
+            }
+
+            boolean foreignKeyModeChanged = configuration.foreignKeyConstraintsEnabled
+                    != mConfiguration.foreignKeyConstraintsEnabled;
+            if (foreignKeyModeChanged) {
+                // Foreign key constraints can only be changed if there are no transactions
+                // in progress.  To make this clear, we throw an exception if there are
+                // any acquired connections.
+                if (!mAcquiredConnections.isEmpty()) {
+                    throw new IllegalStateException("Foreign Key Constraints cannot "
+                            + "be enabled or disabled while there are transactions in "
+                            + "progress.  Finish all transactions and release all active "
+                            + "database connections first.");
+                }
+            }
+
+            // We should do in-place switching when transitioning from compatibility WAL
+            // to rollback journal. Otherwise transient connection state will be lost
+            boolean onlyCompatWalChanged = (mConfiguration.openFlags ^ configuration.openFlags)
+                    == SQLiteDatabase.ENABLE_LEGACY_COMPATIBILITY_WAL;
+
+            if (!onlyCompatWalChanged && mConfiguration.openFlags != configuration.openFlags) {
+                // If we are changing open flags and WAL mode at the same time, then
+                // we have no choice but to close the primary connection beforehand
+                // because there can only be one connection open when we change WAL mode.
+                if (walModeChanged) {
+                    closeAvailableConnectionsAndLogExceptionsLocked();
+                }
+
+                // Try to reopen the primary connection using the new open flags then
+                // close and discard all existing connections.
+                // This might throw if the database is corrupt or cannot be opened in
+                // the new mode in which case existing connections will remain untouched.
+                SQLiteConnection newPrimaryConnection = openConnectionLocked(configuration,
+                        true /*primaryConnection*/); // might throw
+
+                closeAvailableConnectionsAndLogExceptionsLocked();
+                discardAcquiredConnectionsLocked();
+
+                mAvailablePrimaryConnection = newPrimaryConnection;
+                mConfiguration.updateParametersFrom(configuration);
+                setMaxConnectionPoolSizeLocked();
+            } else {
+                // Reconfigure the database connections in place.
+                mConfiguration.updateParametersFrom(configuration);
+                setMaxConnectionPoolSizeLocked();
+
+                closeExcessConnectionsAndLogExceptionsLocked();
+                reconfigureAllConnectionsLocked();
+            }
+
+            wakeConnectionWaitersLocked();
+        }
+    }
+
+    /**
+     * Acquires a connection from the pool.
+     * <p>
+     * The caller must call {@link #releaseConnection} to release the connection
+     * back to the pool when it is finished.  Failure to do so will result
+     * in much unpleasantness.
+     * </p>
+     *
+     * @param sql If not null, try to find a connection that already has
+     * the specified SQL statement in its prepared statement cache.
+     * @param connectionFlags The connection request flags.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * @return The connection that was acquired, never null.
+     *
+     * @throws IllegalStateException if the pool has been closed.
+     * @throws SQLiteException if a database error occurs.
+     * @throws OperationCanceledException if the operation was canceled.
+     */
+    public SQLiteConnection acquireConnection(String sql, int connectionFlags,
+            CancellationSignal cancellationSignal) {
+        SQLiteConnection con = waitForConnection(sql, connectionFlags, cancellationSignal);
+        synchronized (mLock) {
+            if (mIdleConnectionHandler != null) {
+                mIdleConnectionHandler.connectionAcquired(con);
+            }
+        }
+        return con;
+    }
+
+    /**
+     * Releases a connection back to the pool.
+     * <p>
+     * It is ok to call this method after the pool has closed, to release
+     * connections that were still in use at the time of closure.
+     * </p>
+     *
+     * @param connection The connection to release.  Must not be null.
+     *
+     * @throws IllegalStateException if the connection was not acquired
+     * from this pool or if it has already been released.
+     */
+    public void releaseConnection(SQLiteConnection connection) {
+        synchronized (mLock) {
+            if (mIdleConnectionHandler != null) {
+                mIdleConnectionHandler.connectionReleased(connection);
+            }
+            AcquiredConnectionStatus status = mAcquiredConnections.remove(connection);
+            if (status == null) {
+                throw new IllegalStateException("Cannot perform this operation "
+                        + "because the specified connection was not acquired "
+                        + "from this pool or has already been released.");
+            }
+
+            if (!mIsOpen) {
+                closeConnectionAndLogExceptionsLocked(connection);
+            } else if (connection.isPrimaryConnection()) {
+                if (recycleConnectionLocked(connection, status)) {
+                    assert mAvailablePrimaryConnection == null;
+                    mAvailablePrimaryConnection = connection;
+                }
+                wakeConnectionWaitersLocked();
+            } else if (mAvailableNonPrimaryConnections.size() >= mMaxConnectionPoolSize - 1) {
+                closeConnectionAndLogExceptionsLocked(connection);
+            } else {
+                if (recycleConnectionLocked(connection, status)) {
+                    mAvailableNonPrimaryConnections.add(connection);
+                }
+                wakeConnectionWaitersLocked();
+            }
+        }
+    }
+
+    // Can't throw.
+    @GuardedBy("mLock")
+    private boolean recycleConnectionLocked(SQLiteConnection connection,
+            AcquiredConnectionStatus status) {
+        if (status == AcquiredConnectionStatus.RECONFIGURE) {
+            try {
+                connection.reconfigure(mConfiguration); // might throw
+            } catch (RuntimeException ex) {
+                Log.e(TAG, "Failed to reconfigure released connection, closing it: "
+                        + connection, ex);
+                status = AcquiredConnectionStatus.DISCARD;
+            }
+        }
+        if (status == AcquiredConnectionStatus.DISCARD) {
+            closeConnectionAndLogExceptionsLocked(connection);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Returns true if the session should yield the connection due to
+     * contention over available database connections.
+     *
+     * @param connection The connection owned by the session.
+     * @param connectionFlags The connection request flags.
+     * @return True if the session should yield its connection.
+     *
+     * @throws IllegalStateException if the connection was not acquired
+     * from this pool or if it has already been released.
+     */
+    public boolean shouldYieldConnection(SQLiteConnection connection, int connectionFlags) {
+        synchronized (mLock) {
+            if (!mAcquiredConnections.containsKey(connection)) {
+                throw new IllegalStateException("Cannot perform this operation "
+                        + "because the specified connection was not acquired "
+                        + "from this pool or has already been released.");
+            }
+
+            if (!mIsOpen) {
+                return false;
+            }
+
+            return isSessionBlockingImportantConnectionWaitersLocked(
+                    connection.isPrimaryConnection(), connectionFlags);
+        }
+    }
+
+    /**
+     * Collects statistics about database connection memory usage.
+     *
+     * @param dbStatsList The list to populate.
+     */
+    public void collectDbStats(ArrayList<DbStats> dbStatsList) {
+        synchronized (mLock) {
+            if (mAvailablePrimaryConnection != null) {
+                mAvailablePrimaryConnection.collectDbStats(dbStatsList);
+            }
+
+            for (SQLiteConnection connection : mAvailableNonPrimaryConnections) {
+                connection.collectDbStats(dbStatsList);
+            }
+
+            for (SQLiteConnection connection : mAcquiredConnections.keySet()) {
+                connection.collectDbStatsUnsafe(dbStatsList);
+            }
+        }
+    }
+
+    // Might throw.
+    private SQLiteConnection openConnectionLocked(SQLiteDatabaseConfiguration configuration,
+            boolean primaryConnection) {
+        final int connectionId = mNextConnectionId++;
+        return SQLiteConnection.open(this, configuration,
+                connectionId, primaryConnection); // might throw
+    }
+
+    void onConnectionLeaked() {
+        // This code is running inside of the SQLiteConnection finalizer.
+        //
+        // We don't know whether it is just the connection that has been finalized (and leaked)
+        // or whether the connection pool has also been or is about to be finalized.
+        // Consequently, it would be a bad idea to try to grab any locks or to
+        // do any significant work here.  So we do the simplest possible thing and
+        // set a flag.  waitForConnection() periodically checks this flag (when it
+        // times out) so that it can recover from leaked connections and wake
+        // itself or other threads up if necessary.
+        //
+        // You might still wonder why we don't try to do more to wake up the waiters
+        // immediately.  First, as explained above, it would be hard to do safely
+        // unless we started an extra Thread to function as a reference queue.  Second,
+        // this is never supposed to happen in normal operation.  Third, there is no
+        // guarantee that the GC will actually detect the leak in a timely manner so
+        // it's not all that important that we recover from the leak in a timely manner
+        // either.  Fourth, if a badly behaved application finds itself hung waiting for
+        // several seconds while waiting for a leaked connection to be detected and recreated,
+        // then perhaps its authors will have added incentive to fix the problem!
+
+        Log.w(TAG, "A SQLiteConnection object for database '"
+                + mConfiguration.label + "' was leaked!  Please fix your application "
+                + "to end transactions in progress properly and to close the database "
+                + "when it is no longer needed.");
+
+        mConnectionLeaked.set(true);
+    }
+
+    void onStatementExecuted(long executionTimeMs) {
+        mTotalExecutionTimeCounter.addAndGet(executionTimeMs);
+    }
+
+    // Can't throw.
+    @GuardedBy("mLock")
+    private void closeAvailableConnectionsAndLogExceptionsLocked() {
+        closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked();
+
+        if (mAvailablePrimaryConnection != null) {
+            closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection);
+            mAvailablePrimaryConnection = null;
+        }
+    }
+
+    // Can't throw.
+    @GuardedBy("mLock")
+    private boolean closeAvailableConnectionLocked(int connectionId) {
+        final int count = mAvailableNonPrimaryConnections.size();
+        for (int i = count - 1; i >= 0; i--) {
+            SQLiteConnection c = mAvailableNonPrimaryConnections.get(i);
+            if (c.getConnectionId() == connectionId) {
+                closeConnectionAndLogExceptionsLocked(c);
+                mAvailableNonPrimaryConnections.remove(i);
+                return true;
+            }
+        }
+
+        if (mAvailablePrimaryConnection != null
+                && mAvailablePrimaryConnection.getConnectionId() == connectionId) {
+            closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection);
+            mAvailablePrimaryConnection = null;
+            return true;
+        }
+        return false;
+    }
+
+    // Can't throw.
+    @GuardedBy("mLock")
+    private void closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked() {
+        final int count = mAvailableNonPrimaryConnections.size();
+        for (int i = 0; i < count; i++) {
+            closeConnectionAndLogExceptionsLocked(mAvailableNonPrimaryConnections.get(i));
+        }
+        mAvailableNonPrimaryConnections.clear();
+    }
+
+    /**
+     * Close non-primary connections that are not currently in use. This method is safe to use
+     * in finalize block as it doesn't throw RuntimeExceptions.
+     */
+    void closeAvailableNonPrimaryConnectionsAndLogExceptions() {
+        synchronized (mLock) {
+            closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked();
+        }
+    }
+
+    // Can't throw.
+    @GuardedBy("mLock")
+    private void closeExcessConnectionsAndLogExceptionsLocked() {
+        int availableCount = mAvailableNonPrimaryConnections.size();
+        while (availableCount-- > mMaxConnectionPoolSize - 1) {
+            SQLiteConnection connection =
+                    mAvailableNonPrimaryConnections.remove(availableCount);
+            closeConnectionAndLogExceptionsLocked(connection);
+        }
+    }
+
+    // Can't throw.
+    @GuardedBy("mLock")
+    private void closeConnectionAndLogExceptionsLocked(SQLiteConnection connection) {
+        try {
+            connection.close(); // might throw
+            if (mIdleConnectionHandler != null) {
+                mIdleConnectionHandler.connectionClosed(connection);
+            }
+        } catch (RuntimeException ex) {
+            Log.e(TAG, "Failed to close connection, its fate is now in the hands "
+                    + "of the merciful GC: " + connection, ex);
+        }
+    }
+
+    // Can't throw.
+    private void discardAcquiredConnectionsLocked() {
+        markAcquiredConnectionsLocked(AcquiredConnectionStatus.DISCARD);
+    }
+
+    // Can't throw.
+    @GuardedBy("mLock")
+    private void reconfigureAllConnectionsLocked() {
+        if (mAvailablePrimaryConnection != null) {
+            try {
+                mAvailablePrimaryConnection.reconfigure(mConfiguration); // might throw
+            } catch (RuntimeException ex) {
+                Log.e(TAG, "Failed to reconfigure available primary connection, closing it: "
+                        + mAvailablePrimaryConnection, ex);
+                closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection);
+                mAvailablePrimaryConnection = null;
+            }
+        }
+
+        int count = mAvailableNonPrimaryConnections.size();
+        for (int i = 0; i < count; i++) {
+            final SQLiteConnection connection = mAvailableNonPrimaryConnections.get(i);
+            try {
+                connection.reconfigure(mConfiguration); // might throw
+            } catch (RuntimeException ex) {
+                Log.e(TAG, "Failed to reconfigure available non-primary connection, closing it: "
+                        + connection, ex);
+                closeConnectionAndLogExceptionsLocked(connection);
+                mAvailableNonPrimaryConnections.remove(i--);
+                count -= 1;
+            }
+        }
+
+        markAcquiredConnectionsLocked(AcquiredConnectionStatus.RECONFIGURE);
+    }
+
+    // Can't throw.
+    private void markAcquiredConnectionsLocked(AcquiredConnectionStatus status) {
+        if (!mAcquiredConnections.isEmpty()) {
+            ArrayList<SQLiteConnection> keysToUpdate = new ArrayList<SQLiteConnection>(
+                    mAcquiredConnections.size());
+            for (Map.Entry<SQLiteConnection, AcquiredConnectionStatus> entry
+                    : mAcquiredConnections.entrySet()) {
+                AcquiredConnectionStatus oldStatus = entry.getValue();
+                if (status != oldStatus
+                        && oldStatus != AcquiredConnectionStatus.DISCARD) {
+                    keysToUpdate.add(entry.getKey());
+                }
+            }
+            final int updateCount = keysToUpdate.size();
+            for (int i = 0; i < updateCount; i++) {
+                mAcquiredConnections.put(keysToUpdate.get(i), status);
+            }
+        }
+    }
+
+    // Might throw.
+    private SQLiteConnection waitForConnection(String sql, int connectionFlags,
+            CancellationSignal cancellationSignal) {
+        final boolean wantPrimaryConnection =
+                (connectionFlags & CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY) != 0;
+
+        final ConnectionWaiter waiter;
+        final int nonce;
+        synchronized (mLock) {
+            throwIfClosedLocked();
+
+            // Abort if canceled.
+            if (cancellationSignal != null) {
+                cancellationSignal.throwIfCanceled();
+            }
+
+            // Try to acquire a connection.
+            SQLiteConnection connection = null;
+            if (!wantPrimaryConnection) {
+                connection = tryAcquireNonPrimaryConnectionLocked(
+                        sql, connectionFlags); // might throw
+            }
+            if (connection == null) {
+                connection = tryAcquirePrimaryConnectionLocked(connectionFlags); // might throw
+            }
+            if (connection != null) {
+                return connection;
+            }
+
+            // No connections available.  Enqueue a waiter in priority order.
+            final int priority = getPriority(connectionFlags);
+            final long startTime = SystemClock.uptimeMillis();
+            waiter = obtainConnectionWaiterLocked(Thread.currentThread(), startTime,
+                    priority, wantPrimaryConnection, sql, connectionFlags);
+            ConnectionWaiter predecessor = null;
+            ConnectionWaiter successor = mConnectionWaiterQueue;
+            while (successor != null) {
+                if (priority > successor.mPriority) {
+                    waiter.mNext = successor;
+                    break;
+                }
+                predecessor = successor;
+                successor = successor.mNext;
+            }
+            if (predecessor != null) {
+                predecessor.mNext = waiter;
+            } else {
+                mConnectionWaiterQueue = waiter;
+            }
+
+            nonce = waiter.mNonce;
+        }
+
+        // Set up the cancellation listener.
+        if (cancellationSignal != null) {
+            cancellationSignal.setOnCancelListener(new CancellationSignal.OnCancelListener() {
+                @Override
+                public void onCancel() {
+                    synchronized (mLock) {
+                        if (waiter.mNonce == nonce) {
+                            cancelConnectionWaiterLocked(waiter);
+                        }
+                    }
+                }
+            });
+        }
+        try {
+            // Park the thread until a connection is assigned or the pool is closed.
+            // Rethrow an exception from the wait, if we got one.
+            long busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS;
+            long nextBusyTimeoutTime = waiter.mStartTime + busyTimeoutMillis;
+            for (;;) {
+                // Detect and recover from connection leaks.
+                if (mConnectionLeaked.compareAndSet(true, false)) {
+                    synchronized (mLock) {
+                        wakeConnectionWaitersLocked();
+                    }
+                }
+
+                // Wait to be unparked (may already have happened), a timeout, or interruption.
+                LockSupport.parkNanos(this, busyTimeoutMillis * 1000000L);
+
+                // Clear the interrupted flag, just in case.
+                Thread.interrupted();
+
+                // Check whether we are done waiting yet.
+                synchronized (mLock) {
+                    throwIfClosedLocked();
+
+                    final SQLiteConnection connection = waiter.mAssignedConnection;
+                    final RuntimeException ex = waiter.mException;
+                    if (connection != null || ex != null) {
+                        recycleConnectionWaiterLocked(waiter);
+                        if (connection != null) {
+                            return connection;
+                        }
+                        throw ex; // rethrow!
+                    }
+
+                    final long now = SystemClock.uptimeMillis();
+                    if (now < nextBusyTimeoutTime) {
+                        busyTimeoutMillis = now - nextBusyTimeoutTime;
+                    } else {
+                        logConnectionPoolBusyLocked(now - waiter.mStartTime, connectionFlags);
+                        busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS;
+                        nextBusyTimeoutTime = now + busyTimeoutMillis;
+                    }
+                }
+            }
+        } finally {
+            // Remove the cancellation listener.
+            if (cancellationSignal != null) {
+                cancellationSignal.setOnCancelListener(null);
+            }
+        }
+    }
+
+    // Can't throw.
+    @GuardedBy("mLock")
+    private void cancelConnectionWaiterLocked(ConnectionWaiter waiter) {
+        if (waiter.mAssignedConnection != null || waiter.mException != null) {
+            // Waiter is done waiting but has not woken up yet.
+            return;
+        }
+
+        // Waiter must still be waiting.  Dequeue it.
+        ConnectionWaiter predecessor = null;
+        ConnectionWaiter current = mConnectionWaiterQueue;
+        while (current != waiter) {
+            assert current != null;
+            predecessor = current;
+            current = current.mNext;
+        }
+        if (predecessor != null) {
+            predecessor.mNext = waiter.mNext;
+        } else {
+            mConnectionWaiterQueue = waiter.mNext;
+        }
+
+        // Send the waiter an exception and unpark it.
+        waiter.mException = new OperationCanceledException();
+        LockSupport.unpark(waiter.mThread);
+
+        // Check whether removing this waiter will enable other waiters to make progress.
+        wakeConnectionWaitersLocked();
+    }
+
+    // Can't throw.
+    private void logConnectionPoolBusyLocked(long waitMillis, int connectionFlags) {
+        final Thread thread = Thread.currentThread();
+        StringBuilder msg = new StringBuilder();
+        msg.append("The connection pool for database '").append(mConfiguration.label);
+        msg.append("' has been unable to grant a connection to thread ");
+        msg.append(thread.getId()).append(" (").append(thread.getName()).append(") ");
+        msg.append("with flags 0x").append(Integer.toHexString(connectionFlags));
+        msg.append(" for ").append(waitMillis * 0.001f).append(" seconds.\n");
+
+        ArrayList<String> requests = new ArrayList<String>();
+        int activeConnections = 0;
+        int idleConnections = 0;
+        if (!mAcquiredConnections.isEmpty()) {
+            for (SQLiteConnection connection : mAcquiredConnections.keySet()) {
+                String description = connection.describeCurrentOperationUnsafe();
+                if (description != null) {
+                    requests.add(description);
+                    activeConnections += 1;
+                } else {
+                    idleConnections += 1;
+                }
+            }
+        }
+        int availableConnections = mAvailableNonPrimaryConnections.size();
+        if (mAvailablePrimaryConnection != null) {
+            availableConnections += 1;
+        }
+
+        msg.append("Connections: ").append(activeConnections).append(" active, ");
+        msg.append(idleConnections).append(" idle, ");
+        msg.append(availableConnections).append(" available.\n");
+
+        if (!requests.isEmpty()) {
+            msg.append("\nRequests in progress:\n");
+            for (String request : requests) {
+                msg.append("  ").append(request).append("\n");
+            }
+        }
+
+        Log.w(TAG, msg.toString());
+    }
+
+    // Can't throw.
+    @GuardedBy("mLock")
+    private void wakeConnectionWaitersLocked() {
+        // Unpark all waiters that have requests that we can fulfill.
+        // This method is designed to not throw runtime exceptions, although we might send
+        // a waiter an exception for it to rethrow.
+        ConnectionWaiter predecessor = null;
+        ConnectionWaiter waiter = mConnectionWaiterQueue;
+        boolean primaryConnectionNotAvailable = false;
+        boolean nonPrimaryConnectionNotAvailable = false;
+        while (waiter != null) {
+            boolean unpark = false;
+            if (!mIsOpen) {
+                unpark = true;
+            } else {
+                try {
+                    SQLiteConnection connection = null;
+                    if (!waiter.mWantPrimaryConnection && !nonPrimaryConnectionNotAvailable) {
+                        connection = tryAcquireNonPrimaryConnectionLocked(
+                                waiter.mSql, waiter.mConnectionFlags); // might throw
+                        if (connection == null) {
+                            nonPrimaryConnectionNotAvailable = true;
+                        }
+                    }
+                    if (connection == null && !primaryConnectionNotAvailable) {
+                        connection = tryAcquirePrimaryConnectionLocked(
+                                waiter.mConnectionFlags); // might throw
+                        if (connection == null) {
+                            primaryConnectionNotAvailable = true;
+                        }
+                    }
+                    if (connection != null) {
+                        waiter.mAssignedConnection = connection;
+                        unpark = true;
+                    } else if (nonPrimaryConnectionNotAvailable && primaryConnectionNotAvailable) {
+                        // There are no connections available and the pool is still open.
+                        // We cannot fulfill any more connection requests, so stop here.
+                        break;
+                    }
+                } catch (RuntimeException ex) {
+                    // Let the waiter handle the exception from acquiring a connection.
+                    waiter.mException = ex;
+                    unpark = true;
+                }
+            }
+
+            final ConnectionWaiter successor = waiter.mNext;
+            if (unpark) {
+                if (predecessor != null) {
+                    predecessor.mNext = successor;
+                } else {
+                    mConnectionWaiterQueue = successor;
+                }
+                waiter.mNext = null;
+
+                LockSupport.unpark(waiter.mThread);
+            } else {
+                predecessor = waiter;
+            }
+            waiter = successor;
+        }
+    }
+
+    // Might throw.
+    @GuardedBy("mLock")
+    private SQLiteConnection tryAcquirePrimaryConnectionLocked(int connectionFlags) {
+        // If the primary connection is available, acquire it now.
+        SQLiteConnection connection = mAvailablePrimaryConnection;
+        if (connection != null) {
+            mAvailablePrimaryConnection = null;
+            finishAcquireConnectionLocked(connection, connectionFlags); // might throw
+            return connection;
+        }
+
+        // Make sure that the primary connection actually exists and has just been acquired.
+        for (SQLiteConnection acquiredConnection : mAcquiredConnections.keySet()) {
+            if (acquiredConnection.isPrimaryConnection()) {
+                return null;
+            }
+        }
+
+        // Uhoh.  No primary connection!  Either this is the first time we asked
+        // for it, or maybe it leaked?
+        connection = openConnectionLocked(mConfiguration,
+                true /*primaryConnection*/); // might throw
+        finishAcquireConnectionLocked(connection, connectionFlags); // might throw
+        return connection;
+    }
+
+    // Might throw.
+    @GuardedBy("mLock")
+    private SQLiteConnection tryAcquireNonPrimaryConnectionLocked(
+            String sql, int connectionFlags) {
+        // Try to acquire the next connection in the queue.
+        SQLiteConnection connection;
+        final int availableCount = mAvailableNonPrimaryConnections.size();
+        if (availableCount > 1 && sql != null) {
+            // If we have a choice, then prefer a connection that has the
+            // prepared statement in its cache.
+            for (int i = 0; i < availableCount; i++) {
+                connection = mAvailableNonPrimaryConnections.get(i);
+                if (connection.isPreparedStatementInCache(sql)) {
+                    mAvailableNonPrimaryConnections.remove(i);
+                    finishAcquireConnectionLocked(connection, connectionFlags); // might throw
+                    return connection;
+                }
+            }
+        }
+        if (availableCount > 0) {
+            // Otherwise, just grab the next one.
+            connection = mAvailableNonPrimaryConnections.remove(availableCount - 1);
+            finishAcquireConnectionLocked(connection, connectionFlags); // might throw
+            return connection;
+        }
+
+        // Expand the pool if needed.
+        int openConnections = mAcquiredConnections.size();
+        if (mAvailablePrimaryConnection != null) {
+            openConnections += 1;
+        }
+        if (openConnections >= mMaxConnectionPoolSize) {
+            return null;
+        }
+        connection = openConnectionLocked(mConfiguration,
+                false /*primaryConnection*/); // might throw
+        finishAcquireConnectionLocked(connection, connectionFlags); // might throw
+        return connection;
+    }
+
+    // Might throw.
+    @GuardedBy("mLock")
+    private void finishAcquireConnectionLocked(SQLiteConnection connection, int connectionFlags) {
+        try {
+            final boolean readOnly = (connectionFlags & CONNECTION_FLAG_READ_ONLY) != 0;
+            connection.setOnlyAllowReadOnlyOperations(readOnly);
+
+            mAcquiredConnections.put(connection, AcquiredConnectionStatus.NORMAL);
+        } catch (RuntimeException ex) {
+            Log.e(TAG, "Failed to prepare acquired connection for session, closing it: "
+                    + connection +", connectionFlags=" + connectionFlags);
+            closeConnectionAndLogExceptionsLocked(connection);
+            throw ex; // rethrow!
+        }
+    }
+
+    private boolean isSessionBlockingImportantConnectionWaitersLocked(
+            boolean holdingPrimaryConnection, int connectionFlags) {
+        ConnectionWaiter waiter = mConnectionWaiterQueue;
+        if (waiter != null) {
+            final int priority = getPriority(connectionFlags);
+            do {
+                // Only worry about blocked connections that have same or lower priority.
+                if (priority > waiter.mPriority) {
+                    break;
+                }
+
+                // If we are holding the primary connection then we are blocking the waiter.
+                // Likewise, if we are holding a non-primary connection and the waiter
+                // would accept a non-primary connection, then we are blocking the waier.
+                if (holdingPrimaryConnection || !waiter.mWantPrimaryConnection) {
+                    return true;
+                }
+
+                waiter = waiter.mNext;
+            } while (waiter != null);
+        }
+        return false;
+    }
+
+    private static int getPriority(int connectionFlags) {
+        return (connectionFlags & CONNECTION_FLAG_INTERACTIVE) != 0 ? 1 : 0;
+    }
+
+    private void setMaxConnectionPoolSizeLocked() {
+        if (!mConfiguration.isInMemoryDb()
+                && (mConfiguration.openFlags & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0) {
+            mMaxConnectionPoolSize = SQLiteGlobal.getWALConnectionPoolSize();
+        } else {
+            // We don't actually need to always restrict the connection pool size to 1
+            // for non-WAL databases.  There might be reasons to use connection pooling
+            // with other journal modes. However, we should always keep pool size of 1 for in-memory
+            // databases since every :memory: db is separate from another.
+            // For now, enabling connection pooling and using WAL are the same thing in the API.
+            mMaxConnectionPoolSize = 1;
+        }
+    }
+
+    /**
+     * Set up the handler based on the provided looper and timeout.
+     */
+    @VisibleForTesting
+    public void setupIdleConnectionHandler(Looper looper, long timeoutMs) {
+        synchronized (mLock) {
+            mIdleConnectionHandler = new IdleConnectionHandler(looper, timeoutMs);
+        }
+    }
+
+    void disableIdleConnectionHandler() {
+        synchronized (mLock) {
+            mIdleConnectionHandler = null;
+        }
+    }
+
+    private void throwIfClosedLocked() {
+        if (!mIsOpen) {
+            throw new IllegalStateException("Cannot perform this operation "
+                    + "because the connection pool has been closed.");
+        }
+    }
+
+    private ConnectionWaiter obtainConnectionWaiterLocked(Thread thread, long startTime,
+            int priority, boolean wantPrimaryConnection, String sql, int connectionFlags) {
+        ConnectionWaiter waiter = mConnectionWaiterPool;
+        if (waiter != null) {
+            mConnectionWaiterPool = waiter.mNext;
+            waiter.mNext = null;
+        } else {
+            waiter = new ConnectionWaiter();
+        }
+        waiter.mThread = thread;
+        waiter.mStartTime = startTime;
+        waiter.mPriority = priority;
+        waiter.mWantPrimaryConnection = wantPrimaryConnection;
+        waiter.mSql = sql;
+        waiter.mConnectionFlags = connectionFlags;
+        return waiter;
+    }
+
+    private void recycleConnectionWaiterLocked(ConnectionWaiter waiter) {
+        waiter.mNext = mConnectionWaiterPool;
+        waiter.mThread = null;
+        waiter.mSql = null;
+        waiter.mAssignedConnection = null;
+        waiter.mException = null;
+        waiter.mNonce += 1;
+        mConnectionWaiterPool = waiter;
+    }
+
+    /**
+     * Dumps debugging information about this connection pool.
+     *
+     * @param printer The printer to receive the dump, not null.
+     * @param verbose True to dump more verbose information.
+     */
+    public void dump(Printer printer, boolean verbose, ArraySet<String> directories) {
+        Printer indentedPrinter = PrefixPrinter.create(printer, "    ");
+        synchronized (mLock) {
+            if (directories != null) {
+                directories.add(new File(mConfiguration.path).getParent());
+            }
+            boolean isCompatibilityWalEnabled = mConfiguration.isLegacyCompatibilityWalEnabled();
+            printer.println("Connection pool for " + mConfiguration.path + ":");
+            printer.println("  Open: " + mIsOpen);
+            printer.println("  Max connections: " + mMaxConnectionPoolSize);
+            printer.println("  Total execution time: " + mTotalExecutionTimeCounter);
+            printer.println("  Configuration: openFlags=" + mConfiguration.openFlags
+                    + ", isLegacyCompatibilityWalEnabled=" + isCompatibilityWalEnabled
+                    + ", journalMode=" + TextUtils.emptyIfNull(mConfiguration.journalMode)
+                    + ", syncMode=" + TextUtils.emptyIfNull(mConfiguration.syncMode));
+
+            if (isCompatibilityWalEnabled) {
+                printer.println("  Compatibility WAL enabled: wal_syncmode="
+                        + SQLiteCompatibilityWalFlags.getWALSyncMode());
+            }
+            if (mConfiguration.isLookasideConfigSet()) {
+                printer.println("  Lookaside config: sz=" + mConfiguration.lookasideSlotSize
+                        + " cnt=" + mConfiguration.lookasideSlotCount);
+            }
+            if (mConfiguration.idleConnectionTimeoutMs != Long.MAX_VALUE) {
+                printer.println(
+                        "  Idle connection timeout: " + mConfiguration.idleConnectionTimeoutMs);
+            }
+            printer.println("  Available primary connection:");
+            if (mAvailablePrimaryConnection != null) {
+                mAvailablePrimaryConnection.dump(indentedPrinter, verbose);
+            } else {
+                indentedPrinter.println("<none>");
+            }
+
+            printer.println("  Available non-primary connections:");
+            if (!mAvailableNonPrimaryConnections.isEmpty()) {
+                final int count = mAvailableNonPrimaryConnections.size();
+                for (int i = 0; i < count; i++) {
+                    mAvailableNonPrimaryConnections.get(i).dump(indentedPrinter, verbose);
+                }
+            } else {
+                indentedPrinter.println("<none>");
+            }
+
+            printer.println("  Acquired connections:");
+            if (!mAcquiredConnections.isEmpty()) {
+                for (Map.Entry<SQLiteConnection, AcquiredConnectionStatus> entry :
+                        mAcquiredConnections.entrySet()) {
+                    final SQLiteConnection connection = entry.getKey();
+                    connection.dumpUnsafe(indentedPrinter, verbose);
+                    indentedPrinter.println("  Status: " + entry.getValue());
+                }
+            } else {
+                indentedPrinter.println("<none>");
+            }
+
+            printer.println("  Connection waiters:");
+            if (mConnectionWaiterQueue != null) {
+                int i = 0;
+                final long now = SystemClock.uptimeMillis();
+                for (ConnectionWaiter waiter = mConnectionWaiterQueue; waiter != null;
+                        waiter = waiter.mNext, i++) {
+                    indentedPrinter.println(i + ": waited for "
+                            + ((now - waiter.mStartTime) * 0.001f)
+                            + " ms - thread=" + waiter.mThread
+                            + ", priority=" + waiter.mPriority
+                            + ", sql='" + waiter.mSql + "'");
+                }
+            } else {
+                indentedPrinter.println("<none>");
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "SQLiteConnectionPool: " + mConfiguration.path;
+    }
+
+    public String getPath() {
+        return mConfiguration.path;
+    }
+
+    private static final class ConnectionWaiter {
+        public ConnectionWaiter mNext;
+        public Thread mThread;
+        public long mStartTime;
+        public int mPriority;
+        public boolean mWantPrimaryConnection;
+        public String mSql;
+        public int mConnectionFlags;
+        public SQLiteConnection mAssignedConnection;
+        public RuntimeException mException;
+        public int mNonce;
+    }
+
+    private class IdleConnectionHandler extends Handler {
+        private final long mTimeout;
+
+        IdleConnectionHandler(Looper looper, long timeout) {
+            super(looper);
+            mTimeout = timeout;
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            // Skip the (obsolete) message if the handler has changed
+            synchronized (mLock) {
+                if (this != mIdleConnectionHandler) {
+                    return;
+                }
+                if (closeAvailableConnectionLocked(msg.what)) {
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG, "Closed idle connection " + mConfiguration.label + " " + msg.what
+                                + " after " + mTimeout);
+                    }
+                }
+            }
+        }
+
+        void connectionReleased(SQLiteConnection con) {
+            sendEmptyMessageDelayed(con.getConnectionId(), mTimeout);
+        }
+
+        void connectionAcquired(SQLiteConnection con) {
+            // Remove any pending close operations
+            removeMessages(con.getConnectionId());
+        }
+
+        void connectionClosed(SQLiteConnection con) {
+            removeMessages(con.getConnectionId());
+        }
+    }
+}
diff --git a/android/database/sqlite/SQLiteConstraintException.java b/android/database/sqlite/SQLiteConstraintException.java
new file mode 100644
index 0000000..e3119eb
--- /dev/null
+++ b/android/database/sqlite/SQLiteConstraintException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2008 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 android.database.sqlite;
+
+/**
+ * An exception that indicates that an integrity constraint was violated.
+ */
+public class SQLiteConstraintException extends SQLiteException {
+    public SQLiteConstraintException() {}
+
+    public SQLiteConstraintException(String error) {
+        super(error);
+    }
+}
diff --git a/android/database/sqlite/SQLiteCursor.java b/android/database/sqlite/SQLiteCursor.java
new file mode 100644
index 0000000..4559e91
--- /dev/null
+++ b/android/database/sqlite/SQLiteCursor.java
@@ -0,0 +1,302 @@
+/*
+ * 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 android.database.sqlite;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.database.AbstractWindowedCursor;
+import android.database.CursorWindow;
+import android.database.DatabaseUtils;
+import android.os.StrictMode;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A Cursor implementation that exposes results from a query on a
+ * {@link SQLiteDatabase}.
+ *
+ * SQLiteCursor is not internally synchronized so code using a SQLiteCursor from multiple
+ * threads should perform its own synchronization when using the SQLiteCursor.
+ */
+public class SQLiteCursor extends AbstractWindowedCursor {
+    static final String TAG = "SQLiteCursor";
+    static final int NO_COUNT = -1;
+
+    /** The name of the table to edit */
+    @UnsupportedAppUsage
+    private final String mEditTable;
+
+    /** The names of the columns in the rows */
+    private final String[] mColumns;
+
+    /** The query object for the cursor */
+    @UnsupportedAppUsage
+    private final SQLiteQuery mQuery;
+
+    /** The compiled query this cursor came from */
+    private final SQLiteCursorDriver mDriver;
+
+    /** The number of rows in the cursor */
+    private int mCount = NO_COUNT;
+
+    /** The number of rows that can fit in the cursor window, 0 if unknown */
+    private int mCursorWindowCapacity;
+
+    /** A mapping of column names to column indices, to speed up lookups */
+    private Map<String, Integer> mColumnNameMap;
+
+    /** Used to find out where a cursor was allocated in case it never got released. */
+    private final Throwable mStackTrace;
+
+    /** Controls fetching of rows relative to requested position **/
+    private boolean mFillWindowForwardOnly;
+
+    /**
+     * Execute a query and provide access to its result set through a Cursor
+     * interface. For a query such as: {@code SELECT name, birth, phone FROM
+     * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth,
+     * phone) would be in the projection argument and everything from
+     * {@code FROM} onward would be in the params argument.
+     *
+     * @param db a reference to a Database object that is already constructed
+     *     and opened. This param is not used any longer
+     * @param editTable the name of the table used for this query
+     * @param query the rest of the query terms
+     *     cursor is finalized
+     * @deprecated use {@link #SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)} instead
+     */
+    @Deprecated
+    public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver,
+            String editTable, SQLiteQuery query) {
+        this(driver, editTable, query);
+    }
+
+    /**
+     * Execute a query and provide access to its result set through a Cursor
+     * interface. For a query such as: {@code SELECT name, birth, phone FROM
+     * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth,
+     * phone) would be in the projection argument and everything from
+     * {@code FROM} onward would be in the params argument.
+     *
+     * @param editTable the name of the table used for this query
+     * @param query the {@link SQLiteQuery} object associated with this cursor object.
+     */
+    public SQLiteCursor(SQLiteCursorDriver driver, String editTable, SQLiteQuery query) {
+        if (query == null) {
+            throw new IllegalArgumentException("query object cannot be null");
+        }
+        if (StrictMode.vmSqliteObjectLeaksEnabled()) {
+            mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
+        } else {
+            mStackTrace = null;
+        }
+        mDriver = driver;
+        mEditTable = editTable;
+        mColumnNameMap = null;
+        mQuery = query;
+
+        mColumns = query.getColumnNames();
+    }
+
+    /**
+     * Get the database that this cursor is associated with.
+     * @return the SQLiteDatabase that this cursor is associated with.
+     */
+    public SQLiteDatabase getDatabase() {
+        return mQuery.getDatabase();
+    }
+
+    @Override
+    public boolean onMove(int oldPosition, int newPosition) {
+        // Make sure the row at newPosition is present in the window
+        if (mWindow == null || newPosition < mWindow.getStartPosition() ||
+                newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) {
+            fillWindow(newPosition);
+        }
+
+        return true;
+    }
+
+    @Override
+    public int getCount() {
+        if (mCount == NO_COUNT) {
+            fillWindow(0);
+        }
+        return mCount;
+    }
+
+    @UnsupportedAppUsage
+    private void fillWindow(int requiredPos) {
+        clearOrCreateWindow(getDatabase().getPath());
+        try {
+            Preconditions.checkArgumentNonnegative(requiredPos,
+                    "requiredPos cannot be negative, but was " + requiredPos);
+
+            if (mCount == NO_COUNT) {
+                mCount = mQuery.fillWindow(mWindow, requiredPos, requiredPos, true);
+                mCursorWindowCapacity = mWindow.getNumRows();
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "received count(*) from native_fill_window: " + mCount);
+                }
+            } else {
+                int startPos = mFillWindowForwardOnly ? requiredPos : DatabaseUtils
+                        .cursorPickFillWindowStartPosition(requiredPos, mCursorWindowCapacity);
+                mQuery.fillWindow(mWindow, startPos, requiredPos, false);
+            }
+        } catch (RuntimeException ex) {
+            // Close the cursor window if the query failed and therefore will
+            // not produce any results.  This helps to avoid accidentally leaking
+            // the cursor window if the client does not correctly handle exceptions
+            // and fails to close the cursor.
+            closeWindow();
+            throw ex;
+        }
+    }
+
+    @Override
+    public int getColumnIndex(String columnName) {
+        // Create mColumnNameMap on demand
+        if (mColumnNameMap == null) {
+            String[] columns = mColumns;
+            int columnCount = columns.length;
+            HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1);
+            for (int i = 0; i < columnCount; i++) {
+                map.put(columns[i], i);
+            }
+            mColumnNameMap = map;
+        }
+
+        // Hack according to bug 903852
+        final int periodIndex = columnName.lastIndexOf('.');
+        if (periodIndex != -1) {
+            Exception e = new Exception();
+            Log.e(TAG, "requesting column name with table name -- " + columnName, e);
+            columnName = columnName.substring(periodIndex + 1);
+        }
+
+        Integer i = mColumnNameMap.get(columnName);
+        if (i != null) {
+            return i.intValue();
+        } else {
+            return -1;
+        }
+    }
+
+    @Override
+    public String[] getColumnNames() {
+        return mColumns;
+    }
+
+    @Override
+    public void deactivate() {
+        super.deactivate();
+        mDriver.cursorDeactivated();
+    }
+
+    @Override
+    public void close() {
+        super.close();
+        synchronized (this) {
+            mQuery.close();
+            mDriver.cursorClosed();
+        }
+    }
+
+    @Override
+    public boolean requery() {
+        if (isClosed()) {
+            return false;
+        }
+
+        synchronized (this) {
+            if (!mQuery.getDatabase().isOpen()) {
+                return false;
+            }
+
+            if (mWindow != null) {
+                mWindow.clear();
+            }
+            mPos = -1;
+            mCount = NO_COUNT;
+
+            mDriver.cursorRequeried(this);
+        }
+
+        try {
+            return super.requery();
+        } catch (IllegalStateException e) {
+            // for backwards compatibility, just return false
+            Log.w(TAG, "requery() failed " + e.getMessage(), e);
+            return false;
+        }
+    }
+
+    @Override
+    public void setWindow(CursorWindow window) {
+        super.setWindow(window);
+        mCount = NO_COUNT;
+    }
+
+    /**
+     * Changes the selection arguments. The new values take effect after a call to requery().
+     */
+    public void setSelectionArguments(String[] selectionArgs) {
+        mDriver.setBindArguments(selectionArgs);
+    }
+
+    /**
+     * Controls fetching of rows relative to requested position.
+     *
+     * <p>Calling this method defines how rows will be loaded, but it doesn't affect rows that
+     * are already in the window. This setting is preserved if a new window is
+     * {@link #setWindow(CursorWindow) set}
+     *
+     * @param fillWindowForwardOnly if true, rows will be fetched starting from requested position
+     * up to the window's capacity. Default value is false.
+     */
+    public void setFillWindowForwardOnly(boolean fillWindowForwardOnly) {
+        mFillWindowForwardOnly = fillWindowForwardOnly;
+    }
+
+    /**
+     * Release the native resources, if they haven't been released yet.
+     */
+    @Override
+    protected void finalize() {
+        try {
+            // if the cursor hasn't been closed yet, close it first
+            if (mWindow != null) {
+                if (mStackTrace != null) {
+                    String sql = mQuery.getSql();
+                    int len = sql.length();
+                    StrictMode.onSqliteObjectLeaked(
+                        "Finalizing a Cursor that has not been deactivated or closed. " +
+                        "database = " + mQuery.getDatabase().getLabel() +
+                        ", table = " + mEditTable +
+                        ", query = " + sql.substring(0, (len > 1000) ? 1000 : len),
+                        mStackTrace);
+                }
+                close();
+            }
+        } finally {
+            super.finalize();
+        }
+    }
+}
diff --git a/android/database/sqlite/SQLiteCursorDriver.java b/android/database/sqlite/SQLiteCursorDriver.java
new file mode 100644
index 0000000..ad2cdd2
--- /dev/null
+++ b/android/database/sqlite/SQLiteCursorDriver.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2007 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 android.database.sqlite;
+
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+
+/**
+ * A driver for SQLiteCursors that is used to create them and gets notified
+ * by the cursors it creates on significant events in their lifetimes.
+ */
+public interface SQLiteCursorDriver {
+    /**
+     * Executes the query returning a Cursor over the result set.
+     * 
+     * @param factory The CursorFactory to use when creating the Cursors, or
+     *         null if standard SQLiteCursors should be returned.
+     * @return a Cursor over the result set
+     */
+    Cursor query(CursorFactory factory, String[] bindArgs);
+
+    /**
+     * Called by a SQLiteCursor when it is released.
+     */
+    void cursorDeactivated();
+
+    /**
+     * Called by a SQLiteCursor when it is requeried.
+     */
+    void cursorRequeried(Cursor cursor);
+
+    /**
+     * Called by a SQLiteCursor when it it closed to destroy this object as well.
+     */
+    void cursorClosed();
+
+    /**
+     * Set new bind arguments. These will take effect in cursorRequeried().
+     * @param bindArgs the new arguments
+     */
+    public void setBindArguments(String[] bindArgs);
+}
diff --git a/android/database/sqlite/SQLiteCustomFunction.java b/android/database/sqlite/SQLiteCustomFunction.java
new file mode 100644
index 0000000..1ace40d
--- /dev/null
+++ b/android/database/sqlite/SQLiteCustomFunction.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2011 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 android.database.sqlite;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+
+/**
+ * Describes a custom SQL function.
+ *
+ * @hide
+ */
+public final class SQLiteCustomFunction {
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public final String name;
+    @UnsupportedAppUsage
+    public final int numArgs;
+    public final SQLiteDatabase.CustomFunction callback;
+
+    /**
+     * Create custom function.
+     *
+     * @param name The name of the sqlite3 function.
+     * @param numArgs The number of arguments for the function, or -1 to
+     * support any number of arguments.
+     * @param callback The callback to invoke when the function is executed.
+     */
+    public SQLiteCustomFunction(String name, int numArgs,
+            SQLiteDatabase.CustomFunction callback) {
+        if (name == null) {
+            throw new IllegalArgumentException("name must not be null.");
+        }
+
+        this.name = name;
+        this.numArgs = numArgs;
+        this.callback = callback;
+    }
+
+    // Called from native.
+    @SuppressWarnings("unused")
+    @UnsupportedAppUsage
+    private void dispatchCallback(String[] args) {
+        callback.callback(args);
+    }
+}
diff --git a/android/database/sqlite/SQLiteDatabase.java b/android/database/sqlite/SQLiteDatabase.java
new file mode 100644
index 0000000..7c4692c
--- /dev/null
+++ b/android/database/sqlite/SQLiteDatabase.java
@@ -0,0 +1,2878 @@
+/*
+ * 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 android.database.sqlite;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.ActivityThread;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.DatabaseErrorHandler;
+import android.database.DatabaseUtils;
+import android.database.DefaultDatabaseErrorHandler;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDebug.DbStats;
+import android.os.CancellationSignal;
+import android.os.Looper;
+import android.os.OperationCanceledException;
+import android.os.SystemProperties;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.EventLog;
+import android.util.Log;
+import android.util.Pair;
+import android.util.Printer;
+
+import com.android.internal.util.Preconditions;
+
+import dalvik.system.CloseGuard;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.WeakHashMap;
+import java.util.function.BinaryOperator;
+import java.util.function.UnaryOperator;
+
+/**
+ * Exposes methods to manage a SQLite database.
+ *
+ * <p>
+ * SQLiteDatabase has methods to create, delete, execute SQL commands, and
+ * perform other common database management tasks.
+ * </p><p>
+ * See the Notepad sample application in the SDK for an example of creating
+ * and managing a database.
+ * </p><p>
+ * Database names must be unique within an application, not across all applications.
+ * </p>
+ *
+ * <h3>Localized Collation - ORDER BY</h3>
+ * <p>
+ * In addition to SQLite's default <code>BINARY</code> collator, Android supplies
+ * two more, <code>LOCALIZED</code>, which changes with the system's current locale,
+ * and <code>UNICODE</code>, which is the Unicode Collation Algorithm and not tailored
+ * to the current locale.
+ * </p>
+ */
+public final class SQLiteDatabase extends SQLiteClosable {
+    private static final String TAG = "SQLiteDatabase";
+
+    private static final int EVENT_DB_CORRUPT = 75004;
+
+    // By default idle connections are not closed
+    private static final boolean DEBUG_CLOSE_IDLE_CONNECTIONS = SystemProperties
+            .getBoolean("persist.debug.sqlite.close_idle_connections", false);
+
+    // 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<>();
+
+    // Thread-local for database sessions that belong to this database.
+    // Each thread has its own database session.
+    // INVARIANT: Immutable.
+    @UnsupportedAppUsage
+    private final ThreadLocal<SQLiteSession> mThreadSession = ThreadLocal
+            .withInitial(this::createSession);
+
+    // The optional factory to use when creating new Cursors.  May be null.
+    // INVARIANT: Immutable.
+    private final CursorFactory mCursorFactory;
+
+    // Error handler to be used when SQLite returns corruption errors.
+    // INVARIANT: Immutable.
+    private final DatabaseErrorHandler mErrorHandler;
+
+    // Shared database state lock.
+    // This lock guards all of the shared state of the database, such as its
+    // configuration, whether it is open or closed, and so on.  This lock should
+    // be held for as little time as possible.
+    //
+    // The lock MUST NOT be held while attempting to acquire database connections or
+    // while executing SQL statements on behalf of the client as it can lead to deadlock.
+    //
+    // It is ok to hold the lock while reconfiguring the connection pool or dumping
+    // statistics because those operations are non-reentrant and do not try to acquire
+    // connections that might be held by other threads.
+    //
+    // Basic rule: grab the lock, access or modify global state, release the lock, then
+    // do the required SQL work.
+    private final Object mLock = new Object();
+
+    // Warns if the database is finalized without being closed properly.
+    // INVARIANT: Guarded by mLock.
+    private final CloseGuard mCloseGuardLocked = CloseGuard.get();
+
+    // The database configuration.
+    // INVARIANT: Guarded by mLock.
+    @UnsupportedAppUsage
+    private final SQLiteDatabaseConfiguration mConfigurationLocked;
+
+    // The connection pool for the database, null when closed.
+    // The pool itself is thread-safe, but the reference to it can only be acquired
+    // when the lock is held.
+    // INVARIANT: Guarded by mLock.
+    @UnsupportedAppUsage
+    private SQLiteConnectionPool mConnectionPoolLocked;
+
+    // True if the database has attached databases.
+    // INVARIANT: Guarded by mLock.
+    private boolean mHasAttachedDbsLocked;
+
+    /**
+     * 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;
+
+    /** {@hide} */
+    @UnsupportedAppUsage
+    public 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;
+
+    /**
+     * Open flag: 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
+
+    /**
+     * Open flag: 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
+
+    /**
+     * Open flag: 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
+
+    /**
+     * Open flag: 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
+
+    /**
+     * Open flag: Flag for {@link #openDatabase} to open the database file with
+     * write-ahead logging enabled by default.  Using this flag is more efficient
+     * than calling {@link #enableWriteAheadLogging}.
+     *
+     * Write-ahead logging cannot be used with read-only databases so the value of
+     * this flag is ignored if the database is opened read-only.
+     *
+     * @see #enableWriteAheadLogging
+     */
+    public static final int ENABLE_WRITE_AHEAD_LOGGING = 0x20000000;
+
+
+    // Note: The below value was only used on Android Pie.
+    // public static final int DISABLE_COMPATIBILITY_WAL = 0x40000000;
+
+    /**
+     * Open flag: Flag for {@link #openDatabase} to enable the legacy Compatibility WAL when opening
+     * database.
+     *
+     * @hide
+     */
+    public static final int ENABLE_LEGACY_COMPATIBILITY_WAL = 0x80000000;
+
+    /**
+     * Absolute max value that can be set by {@link #setMaxSqlCacheSize(int)}.
+     *
+     * Each prepared-statement is between 1K - 6K, depending on the complexity of the
+     * SQL statement & schema.  A large SQL cache may use a significant amount of memory.
+     */
+    public static final int MAX_SQL_CACHE_SIZE = 100;
+
+    private SQLiteDatabase(final String path, final int openFlags,
+            CursorFactory cursorFactory, DatabaseErrorHandler errorHandler,
+            int lookasideSlotSize, int lookasideSlotCount, long idleConnectionTimeoutMs,
+            String journalMode, String syncMode) {
+        mCursorFactory = cursorFactory;
+        mErrorHandler = errorHandler != null ? errorHandler : new DefaultDatabaseErrorHandler();
+        mConfigurationLocked = new SQLiteDatabaseConfiguration(path, openFlags);
+        mConfigurationLocked.lookasideSlotSize = lookasideSlotSize;
+        mConfigurationLocked.lookasideSlotCount = lookasideSlotCount;
+        // Disable lookaside allocator on low-RAM devices
+        if (ActivityManager.isLowRamDeviceStatic()) {
+            mConfigurationLocked.lookasideSlotCount = 0;
+            mConfigurationLocked.lookasideSlotSize = 0;
+        }
+        long effectiveTimeoutMs = Long.MAX_VALUE;
+        // Never close idle connections for in-memory databases
+        if (!mConfigurationLocked.isInMemoryDb()) {
+            // First, check app-specific value. Otherwise use defaults
+            // -1 in idleConnectionTimeoutMs indicates unset value
+            if (idleConnectionTimeoutMs >= 0) {
+                effectiveTimeoutMs = idleConnectionTimeoutMs;
+            } else if (DEBUG_CLOSE_IDLE_CONNECTIONS) {
+                effectiveTimeoutMs = SQLiteGlobal.getIdleConnectionTimeout();
+            }
+        }
+        mConfigurationLocked.idleConnectionTimeoutMs = effectiveTimeoutMs;
+        mConfigurationLocked.journalMode = journalMode;
+        mConfigurationLocked.syncMode = syncMode;
+        if (SQLiteCompatibilityWalFlags.isLegacyCompatibilityWalEnabled()) {
+            mConfigurationLocked.openFlags |= ENABLE_LEGACY_COMPATIBILITY_WAL;
+        }
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            dispose(true);
+        } finally {
+            super.finalize();
+        }
+    }
+
+    @Override
+    protected void onAllReferencesReleased() {
+        dispose(false);
+    }
+
+    private void dispose(boolean finalized) {
+        final SQLiteConnectionPool pool;
+        synchronized (mLock) {
+            if (mCloseGuardLocked != null) {
+                if (finalized) {
+                    mCloseGuardLocked.warnIfOpen();
+                }
+                mCloseGuardLocked.close();
+            }
+
+            pool = mConnectionPoolLocked;
+            mConnectionPoolLocked = null;
+        }
+
+        if (!finalized) {
+            synchronized (sActiveDatabases) {
+                sActiveDatabases.remove(this);
+            }
+
+            if (pool != null) {
+                pool.close();
+            }
+        }
+    }
+
+    /**
+     * 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
+     */
+    public static int releaseMemory() {
+        return SQLiteGlobal.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
+     *
+     * @deprecated This method now does nothing.  Do not use.
+     */
+    @Deprecated
+    public void setLockingEnabled(boolean lockingEnabled) {
+    }
+
+    /**
+     * Gets a label to use when describing the database in log messages.
+     * @return The label.
+     */
+    String getLabel() {
+        synchronized (mLock) {
+            return mConfigurationLocked.label;
+        }
+    }
+
+    /**
+     * Sends a corruption message to the database error handler.
+     */
+    void onCorruption() {
+        EventLog.writeEvent(EVENT_DB_CORRUPT, getLabel());
+        mErrorHandler.onCorruption(this);
+    }
+
+    /**
+     * Gets the {@link SQLiteSession} that belongs to this thread for this database.
+     * Once a thread has obtained a session, it will continue to obtain the same
+     * session even after the database has been closed (although the session will not
+     * be usable).  However, a thread that does not already have a session cannot
+     * obtain one after the database has been closed.
+     *
+     * The idea is that threads that have active connections to the database may still
+     * have work to complete even after the call to {@link #close}.  Active database
+     * connections are not actually disposed until they are released by the threads
+     * that own them.
+     *
+     * @return The session, never null.
+     *
+     * @throws IllegalStateException if the thread does not yet have a session and
+     * the database is not open.
+     */
+    @UnsupportedAppUsage
+    SQLiteSession getThreadSession() {
+        return mThreadSession.get(); // initialValue() throws if database closed
+    }
+
+    SQLiteSession createSession() {
+        final SQLiteConnectionPool pool;
+        synchronized (mLock) {
+            throwIfNotOpenLocked();
+            pool = mConnectionPoolLocked;
+        }
+        return new SQLiteSession(pool);
+    }
+
+    /**
+     * Gets default connection flags that are appropriate for this thread, taking into
+     * account whether the thread is acting on behalf of the UI.
+     *
+     * @param readOnly True if the connection should be read-only.
+     * @return The connection flags.
+     */
+    int getThreadDefaultConnectionFlags(boolean readOnly) {
+        int flags = readOnly ? SQLiteConnectionPool.CONNECTION_FLAG_READ_ONLY :
+                SQLiteConnectionPool.CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY;
+        if (isMainThread()) {
+            flags |= SQLiteConnectionPool.CONNECTION_FLAG_INTERACTIVE;
+        }
+        return flags;
+    }
+
+    private static boolean isMainThread() {
+        // FIXME: There should be a better way to do this.
+        // Would also be nice to have something that would work across Binder calls.
+        Looper looper = Looper.myLooper();
+        return looper != null && looper == Looper.getMainLooper();
+    }
+
+    /**
+     * Begins a transaction in EXCLUSIVE mode.
+     * <p>
+     * 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>
+     * <p>Here is the standard idiom for transactions:
+     *
+     * <pre>
+     *   db.beginTransaction();
+     *   try {
+     *     ...
+     *     db.setTransactionSuccessful();
+     *   } finally {
+     *     db.endTransaction();
+     *   }
+     * </pre>
+     */
+    public void beginTransaction() {
+        beginTransaction(null /* transactionStatusCallback */, true);
+    }
+
+    /**
+     * Begins a transaction in IMMEDIATE 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.beginTransactionNonExclusive();
+     *   try {
+     *     ...
+     *     db.setTransactionSuccessful();
+     *   } finally {
+     *     db.endTransaction();
+     *   }
+     * </pre>
+     */
+    public void beginTransactionNonExclusive() {
+        beginTransaction(null /* transactionStatusCallback */, false);
+    }
+
+    /**
+     * Begins a transaction in EXCLUSIVE mode.
+     * <p>
+     * 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>
+     * <p>Here is the standard idiom for transactions:
+     *
+     * <pre>
+     *   db.beginTransactionWithListener(listener);
+     *   try {
+     *     ...
+     *     db.setTransactionSuccessful();
+     *   } finally {
+     *     db.endTransaction();
+     *   }
+     * </pre>
+     *
+     * @param transactionListener listener that should be notified when the transaction begins,
+     * commits, or is rolled back, either explicitly or by a call to
+     * {@link #yieldIfContendedSafely}.
+     */
+    public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) {
+        beginTransaction(transactionListener, true);
+    }
+
+    /**
+     * Begins a transaction in IMMEDIATE 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.beginTransactionWithListenerNonExclusive(listener);
+     *   try {
+     *     ...
+     *     db.setTransactionSuccessful();
+     *   } finally {
+     *     db.endTransaction();
+     *   }
+     * </pre>
+     *
+     * @param transactionListener listener that should be notified when the
+     *            transaction begins, commits, or is rolled back, either
+     *            explicitly or by a call to {@link #yieldIfContendedSafely}.
+     */
+    public void beginTransactionWithListenerNonExclusive(
+            SQLiteTransactionListener transactionListener) {
+        beginTransaction(transactionListener, false);
+    }
+
+    @UnsupportedAppUsage
+    private void beginTransaction(SQLiteTransactionListener transactionListener,
+            boolean exclusive) {
+        acquireReference();
+        try {
+            getThreadSession().beginTransaction(
+                    exclusive ? SQLiteSession.TRANSACTION_MODE_EXCLUSIVE :
+                            SQLiteSession.TRANSACTION_MODE_IMMEDIATE,
+                    transactionListener,
+                    getThreadDefaultConnectionFlags(false /*readOnly*/), null);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * End a transaction. See beginTransaction for notes about how to use this and when transactions
+     * are committed and rolled back.
+     */
+    public void endTransaction() {
+        acquireReference();
+        try {
+            getThreadSession().endTransaction(null);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Marks the current transaction as successful. Do not do any more database work between
+     * calling this and calling endTransaction. Do as little non-database work as possible in that
+     * situation too. If any errors are encountered between this and endTransaction the transaction
+     * will still be committed.
+     *
+     * @throws IllegalStateException if the current thread is not in a transaction or the
+     * transaction is already marked as successful.
+     */
+    public void setTransactionSuccessful() {
+        acquireReference();
+        try {
+            getThreadSession().setTransactionSuccessful();
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Returns true if the current thread has a transaction pending.
+     *
+     * @return True if the current thread is in a transaction.
+     */
+    public boolean inTransaction() {
+        acquireReference();
+        try {
+            return getThreadSession().hasTransaction();
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Returns true if the current thread is holding an active connection to the database.
+     * <p>
+     * The name of this method comes from a time when having an active connection
+     * to the database meant that the thread was holding an actual lock on the
+     * database.  Nowadays, there is no longer a true "database lock" although threads
+     * may block if they cannot acquire a database connection to perform a
+     * particular operation.
+     * </p>
+     *
+     * @return True if the current thread is holding an active connection to the database.
+     */
+    public boolean isDbLockedByCurrentThread() {
+        acquireReference();
+        try {
+            return getThreadSession().hasConnection();
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Always returns false.
+     * <p>
+     * There is no longer the concept of a database lock, so this method always returns false.
+     * </p>
+     *
+     * @return False.
+     * @deprecated Always returns false.  Do not use this method.
+     */
+    @Deprecated
+    public boolean isDbLockedByOtherThreads() {
+        return false;
+    }
+
+    /**
+     * 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 (because of nested transactions) then the lock
+     *   will not be yielded. Use yieldIfContendedSafely instead.
+     */
+    @Deprecated
+    public boolean yieldIfContended() {
+        return yieldIfContendedHelper(false /* do not check yielding */,
+                -1 /* sleepAfterYieldDelay */);
+    }
+
+    /**
+     * Temporarily end the transaction to let other threads run. The transaction is assumed to be
+     * successful so far. Do not call setTransactionSuccessful before calling this. When this
+     * returns a new transaction will have been created but not marked as successful. This assumes
+     * that there are no nested transactions (beginTransaction has only been called once) and will
+     * throw an exception if that is not the case.
+     * @return true if the transaction was yielded
+     */
+    public boolean yieldIfContendedSafely() {
+        return yieldIfContendedHelper(true /* check yielding */, -1 /* sleepAfterYieldDelay*/);
+    }
+
+    /**
+     * Temporarily end the transaction to let other threads run. The transaction is assumed to be
+     * successful so far. Do not call setTransactionSuccessful before calling this. When this
+     * returns a new transaction will have been created but not marked as successful. This assumes
+     * that there are no nested transactions (beginTransaction has only been called once) and will
+     * throw an exception if that is not the case.
+     * @param sleepAfterYieldDelay if > 0, sleep this long before starting a new transaction if
+     *   the lock was actually yielded. This will allow other background threads to make some
+     *   more progress than they would if we started the transaction immediately.
+     * @return true if the transaction was yielded
+     */
+    public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) {
+        return yieldIfContendedHelper(true /* check yielding */, sleepAfterYieldDelay);
+    }
+
+    private boolean yieldIfContendedHelper(boolean throwIfUnsafe, long sleepAfterYieldDelay) {
+        acquireReference();
+        try {
+            return getThreadSession().yieldTransaction(sleepAfterYieldDelay, throwIfUnsafe, null);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Deprecated.
+     * @deprecated This method no longer serves any useful purpose and has been deprecated.
+     */
+    @Deprecated
+    public Map<String, String> getSyncedTables() {
+        return new HashMap<String, String>(0);
+    }
+
+    /**
+     * Open the database according to the flags {@link #OPEN_READWRITE}
+     * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}.
+     *
+     * <p>Sets the locale of the database to the  the system's current locale.
+     * Call {@link #setLocale} if you would like something else.</p>
+     *
+     * @param path to database file to open and/or create
+     * @param factory an optional factory class that is called to instantiate a
+     *            cursor when query is called, or null for default
+     * @param flags to control database access mode
+     * @return the newly opened database
+     * @throws SQLiteException if the database cannot be opened
+     */
+    public static SQLiteDatabase openDatabase(@NonNull String path, @Nullable CursorFactory factory,
+            @DatabaseOpenFlags int flags) {
+        return openDatabase(path, factory, flags, null);
+    }
+
+    /**
+     * Open the database according to the specified {@link OpenParams parameters}
+     *
+     * @param path path to database file to open and/or create.
+     * <p><strong>Important:</strong> The file should be constructed either from an absolute path or
+     * by using {@link android.content.Context#getDatabasePath(String)}.
+     * @param openParams configuration parameters that are used for opening {@link SQLiteDatabase}
+     * @return the newly opened database
+     * @throws SQLiteException if the database cannot be opened
+     */
+    public static SQLiteDatabase openDatabase(@NonNull File path,
+            @NonNull OpenParams openParams) {
+        return openDatabase(path.getPath(), openParams);
+    }
+
+    @UnsupportedAppUsage
+    private static SQLiteDatabase openDatabase(@NonNull String path,
+            @NonNull OpenParams openParams) {
+        Preconditions.checkArgument(openParams != null, "OpenParams cannot be null");
+        SQLiteDatabase db = new SQLiteDatabase(path, openParams.mOpenFlags,
+                openParams.mCursorFactory, openParams.mErrorHandler,
+                openParams.mLookasideSlotSize, openParams.mLookasideSlotCount,
+                openParams.mIdleConnectionTimeout, openParams.mJournalMode, openParams.mSyncMode);
+        db.open();
+        return db;
+    }
+
+    /**
+     * 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>
+     *
+     * <p>Accepts input param: a concrete instance of {@link DatabaseErrorHandler} to be
+     * used to handle corruption when sqlite reports database corruption.</p>
+     *
+     * @param path to database file to open and/or create
+     * @param factory an optional factory class that is called to instantiate a
+     *            cursor when query is called, or null for default
+     * @param flags to control database access mode
+     * @param errorHandler the {@link DatabaseErrorHandler} obj to be used to handle corruption
+     * when sqlite reports database corruption
+     * @return the newly opened database
+     * @throws SQLiteException if the database cannot be opened
+     */
+    public static SQLiteDatabase openDatabase(@NonNull String path, @Nullable CursorFactory factory,
+            @DatabaseOpenFlags int flags, @Nullable DatabaseErrorHandler errorHandler) {
+        SQLiteDatabase db = new SQLiteDatabase(path, flags, factory, errorHandler, -1, -1, -1, null,
+                null);
+        db.open();
+        return db;
+    }
+
+    /**
+     * Equivalent to openDatabase(file.getPath(), factory, CREATE_IF_NECESSARY).
+     */
+    public static SQLiteDatabase openOrCreateDatabase(@NonNull File file,
+            @Nullable CursorFactory factory) {
+        return openOrCreateDatabase(file.getPath(), factory);
+    }
+
+    /**
+     * Equivalent to openDatabase(path, factory, CREATE_IF_NECESSARY).
+     */
+    public static SQLiteDatabase openOrCreateDatabase(@NonNull String path,
+            @Nullable CursorFactory factory) {
+        return openDatabase(path, factory, CREATE_IF_NECESSARY, null);
+    }
+
+    /**
+     * Equivalent to openDatabase(path, factory, CREATE_IF_NECESSARY, errorHandler).
+     */
+    public static SQLiteDatabase openOrCreateDatabase(@NonNull String path,
+            @Nullable CursorFactory factory, @Nullable DatabaseErrorHandler errorHandler) {
+        return openDatabase(path, factory, CREATE_IF_NECESSARY, errorHandler);
+    }
+
+    /**
+     * Deletes a database including its journal file and other auxiliary files
+     * that may have been created by the database engine.
+     *
+     * @param file The database file path.
+     * @return True if the database was successfully deleted.
+     */
+    public static boolean deleteDatabase(@NonNull File file) {
+        return deleteDatabase(file, /*removeCheckFile=*/ true);
+    }
+
+
+    /** @hide */
+    public static boolean deleteDatabase(@NonNull File file, boolean removeCheckFile) {
+        if (file == null) {
+            throw new IllegalArgumentException("file must not be null");
+        }
+
+        boolean deleted = false;
+        deleted |= file.delete();
+        deleted |= new File(file.getPath() + "-journal").delete();
+        deleted |= new File(file.getPath() + "-shm").delete();
+        deleted |= new File(file.getPath() + "-wal").delete();
+
+        // This file is not a standard SQLite file, so don't update the deleted flag.
+        new File(file.getPath() + SQLiteGlobal.WIPE_CHECK_FILE_SUFFIX).delete();
+
+        File dir = file.getParentFile();
+        if (dir != null) {
+            final String prefix = file.getName() + "-mj";
+            File[] files = dir.listFiles(new FileFilter() {
+                @Override
+                public boolean accept(File candidate) {
+                    return candidate.getName().startsWith(prefix);
+                }
+            });
+            if (files != null) {
+                for (File masterJournal : files) {
+                    deleted |= masterJournal.delete();
+                }
+            }
+        }
+        return deleted;
+    }
+
+    /**
+     * Reopens the database in read-write mode.
+     * If the database is already read-write, does nothing.
+     *
+     * @throws SQLiteException if the database could not be reopened as requested, in which
+     * case it remains open in read only mode.
+     * @throws IllegalStateException if the database is not open.
+     *
+     * @see #isReadOnly()
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void reopenReadWrite() {
+        synchronized (mLock) {
+            throwIfNotOpenLocked();
+
+            if (!isReadOnlyLocked()) {
+                return; // nothing to do
+            }
+
+            // Reopen the database in read-write mode.
+            final int oldOpenFlags = mConfigurationLocked.openFlags;
+            mConfigurationLocked.openFlags = (mConfigurationLocked.openFlags & ~OPEN_READ_MASK)
+                    | OPEN_READWRITE;
+            try {
+                mConnectionPoolLocked.reconfigure(mConfigurationLocked);
+            } catch (RuntimeException ex) {
+                mConfigurationLocked.openFlags = oldOpenFlags;
+                throw ex;
+            }
+        }
+    }
+
+    private void open() {
+        try {
+            try {
+                openInner();
+            } catch (RuntimeException ex) {
+                if (SQLiteDatabaseCorruptException.isCorruptException(ex)) {
+                    Log.e(TAG, "Database corruption detected in open()", ex);
+                    onCorruption();
+                    openInner();
+                } else {
+                    throw ex;
+                }
+            }
+        } catch (SQLiteException ex) {
+            Log.e(TAG, "Failed to open database '" + getLabel() + "'.", ex);
+            close();
+            throw ex;
+        }
+    }
+
+    private void openInner() {
+        synchronized (mLock) {
+            assert mConnectionPoolLocked == null;
+            mConnectionPoolLocked = SQLiteConnectionPool.open(mConfigurationLocked);
+            mCloseGuardLocked.open("close");
+        }
+
+        synchronized (sActiveDatabases) {
+            sActiveDatabases.put(this, null);
+        }
+    }
+
+    /**
+     * Create a memory backed SQLite database.  Its contents will be destroyed
+     * when the database is closed.
+     *
+     * <p>Sets the locale of the database to the  the system's current locale.
+     * Call {@link #setLocale} if you would like something else.</p>
+     *
+     * @param factory an optional factory class that is called to instantiate a
+     *            cursor when query is called
+     * @return a SQLiteDatabase instance
+     * @throws SQLiteException if the database cannot be created
+     */
+    @NonNull
+    public static SQLiteDatabase create(@Nullable CursorFactory factory) {
+        // This is a magic string with special meaning for SQLite.
+        return openDatabase(SQLiteDatabaseConfiguration.MEMORY_DB_PATH,
+                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 openParams configuration parameters that are used for opening SQLiteDatabase
+     * @return a SQLiteDatabase instance
+     * @throws SQLException if the database cannot be created
+     */
+    @NonNull
+    public static SQLiteDatabase createInMemory(@NonNull OpenParams openParams) {
+        return openDatabase(SQLiteDatabaseConfiguration.MEMORY_DB_PATH,
+                openParams.toBuilder().addOpenFlags(CREATE_IF_NECESSARY).build());
+    }
+
+    /**
+     * Register a custom scalar function that can be called from SQL
+     * expressions.
+     * <p>
+     * For example, registering a custom scalar function named {@code REVERSE}
+     * could be used in a query like
+     * {@code SELECT REVERSE(name) FROM employees}.
+     * <p>
+     * When attempting to register multiple functions with the same function
+     * name, SQLite will replace any previously defined functions with the
+     * latest definition, regardless of what function type they are. SQLite does
+     * not support unregistering functions.
+     *
+     * @param functionName Case-insensitive name to register this function
+     *            under, limited to 255 UTF-8 bytes in length.
+     * @param scalarFunction Functional interface that will be invoked when the
+     *            function name is used by a SQL statement. The argument values
+     *            from the SQL statement are passed to the functional interface,
+     *            and the return values from the functional interface are
+     *            returned back into the SQL statement.
+     * @throws SQLiteException if the custom function could not be registered.
+     * @see #setCustomAggregateFunction(String, BinaryOperator)
+     */
+    public void setCustomScalarFunction(@NonNull String functionName,
+            @NonNull UnaryOperator<String> scalarFunction) throws SQLiteException {
+        Objects.requireNonNull(functionName);
+        Objects.requireNonNull(scalarFunction);
+
+        synchronized (mLock) {
+            throwIfNotOpenLocked();
+
+            mConfigurationLocked.customScalarFunctions.put(functionName, scalarFunction);
+            try {
+                mConnectionPoolLocked.reconfigure(mConfigurationLocked);
+            } catch (RuntimeException ex) {
+                mConfigurationLocked.customScalarFunctions.remove(functionName);
+                throw ex;
+            }
+        }
+    }
+
+    /**
+     * Register a custom aggregate function that can be called from SQL
+     * expressions.
+     * <p>
+     * For example, registering a custom aggregation function named
+     * {@code LONGEST} could be used in a query like
+     * {@code SELECT LONGEST(name) FROM employees}.
+     * <p>
+     * The implementation of this method follows the reduction flow outlined in
+     * {@link java.util.stream.Stream#reduce(BinaryOperator)}, and the custom
+     * aggregation function is expected to be an associative accumulation
+     * function, as defined by that class.
+     * <p>
+     * When attempting to register multiple functions with the same function
+     * name, SQLite will replace any previously defined functions with the
+     * latest definition, regardless of what function type they are. SQLite does
+     * not support unregistering functions.
+     *
+     * @param functionName Case-insensitive name to register this function
+     *            under, limited to 255 UTF-8 bytes in length.
+     * @param aggregateFunction Functional interface that will be invoked when
+     *            the function name is used by a SQL statement. The argument
+     *            values from the SQL statement are passed to the functional
+     *            interface, and the return values from the functional interface
+     *            are returned back into the SQL statement.
+     * @throws SQLiteException if the custom function could not be registered.
+     * @see #setCustomScalarFunction(String, UnaryOperator)
+     */
+    public void setCustomAggregateFunction(@NonNull String functionName,
+            @NonNull BinaryOperator<String> aggregateFunction) throws SQLiteException {
+        Objects.requireNonNull(functionName);
+        Objects.requireNonNull(aggregateFunction);
+
+        synchronized (mLock) {
+            throwIfNotOpenLocked();
+
+            mConfigurationLocked.customAggregateFunctions.put(functionName, aggregateFunction);
+            try {
+                mConnectionPoolLocked.reconfigure(mConfigurationLocked);
+            } catch (RuntimeException ex) {
+                mConfigurationLocked.customAggregateFunctions.remove(functionName);
+                throw ex;
+            }
+        }
+    }
+
+    /**
+     * Execute the given SQL statement on all connections to this database.
+     * <p>
+     * This statement will be immediately executed on all existing connections,
+     * and will be automatically executed on all future connections.
+     * <p>
+     * Some example usages are changes like {@code PRAGMA trusted_schema=OFF} or
+     * functions like {@code SELECT icu_load_collation()}. If you execute these
+     * statements using {@link #execSQL} then they will only apply to a single
+     * database connection; using this method will ensure that they are
+     * uniformly applied to all current and future connections.
+     *
+     * @param sql The SQL statement to be executed. Multiple statements
+     *            separated by semicolons are not supported.
+     * @param bindArgs The arguments that should be bound to the SQL statement.
+     */
+    public void execPerConnectionSQL(@NonNull String sql, @Nullable Object[] bindArgs)
+            throws SQLException {
+        Objects.requireNonNull(sql);
+
+        // Copy arguments to ensure that the caller doesn't accidentally change
+        // the values used by future connections
+        bindArgs = DatabaseUtils.deepCopyOf(bindArgs);
+
+        synchronized (mLock) {
+            throwIfNotOpenLocked();
+
+            final int index = mConfigurationLocked.perConnectionSql.size();
+            mConfigurationLocked.perConnectionSql.add(Pair.create(sql, bindArgs));
+            try {
+                mConnectionPoolLocked.reconfigure(mConfigurationLocked);
+            } catch (RuntimeException ex) {
+                mConfigurationLocked.perConnectionSql.remove(index);
+                throw ex;
+            }
+        }
+    }
+
+    /**
+     * Gets the database version.
+     *
+     * @return the database version
+     */
+    public int getVersion() {
+        return ((Long) DatabaseUtils.longForQuery(this, "PRAGMA user_version;", null)).intValue();
+    }
+
+    /**
+     * Sets the database version.
+     *
+     * @param version the new database version
+     */
+    public void setVersion(int version) {
+        execSQL("PRAGMA user_version = " + version);
+    }
+
+    /**
+     * Returns the maximum size the database may grow to.
+     *
+     * @return the new maximum database size
+     */
+    public long getMaximumSize() {
+        long pageCount = DatabaseUtils.longForQuery(this, "PRAGMA max_page_count;", null);
+        return pageCount * getPageSize();
+    }
+
+    /**
+     * 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) {
+        long pageSize = getPageSize();
+        long numPages = numBytes / pageSize;
+        // If numBytes isn't a multiple of pageSize, bump up a page
+        if ((numBytes % pageSize) != 0) {
+            numPages++;
+        }
+        long newPageCount = DatabaseUtils.longForQuery(this, "PRAGMA max_page_count = " + numPages,
+                null);
+        return newPageCount * pageSize;
+    }
+
+    /**
+     * Returns the current database page size, in bytes.
+     *
+     * @return the database page size, in bytes
+     */
+    public long getPageSize() {
+        return DatabaseUtils.longForQuery(this, "PRAGMA page_size;", null);
+    }
+
+    /**
+     * 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
+     * @deprecated This method no longer serves any useful purpose and has been deprecated.
+     */
+    @Deprecated
+    public void markTableSyncable(String table, String 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
+     * @deprecated This method no longer serves any useful purpose and has been deprecated.
+     */
+    @Deprecated
+    public void markTableSyncable(String table, String foreignKey, String updateTable) {
+    }
+
+    /**
+     * 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.
+     *<p>
+     * No two threads should be using the same {@link SQLiteStatement} at the same time.
+     *
+     * @param sql The raw SQL statement, may contain ? for unknown values to be
+     *            bound later.
+     * @return A pre-compiled {@link SQLiteStatement} object. Note that
+     * {@link SQLiteStatement}s are not synchronized, see the documentation for more details.
+     */
+    public SQLiteStatement compileStatement(String sql) throws SQLException {
+        acquireReference();
+        try {
+            return new SQLiteStatement(this, sql, null);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Query the given URL, returning a {@link Cursor} over the result set.
+     *
+     * @param distinct true if you want each row to be unique, false otherwise.
+     * @param table The table name to compile the query against.
+     * @param columns A list of which columns to return. Passing null will
+     *            return all columns, which is discouraged to prevent reading
+     *            data from storage that isn't going to be used.
+     * @param selection A filter declaring which rows to return, formatted as an
+     *            SQL WHERE clause (excluding the WHERE itself). Passing null
+     *            will return all rows for the given table.
+     * @param selectionArgs You may include ?s in selection, which will be
+     *         replaced by the values from selectionArgs, in order that they
+     *         appear in the selection. The values will be bound as Strings.
+     * @param groupBy A filter declaring how to group rows, formatted as an SQL
+     *            GROUP BY clause (excluding the GROUP BY itself). Passing null
+     *            will cause the rows to not be grouped.
+     * @param having A filter declare which row groups to include in the cursor,
+     *            if row grouping is being used, formatted as an SQL HAVING
+     *            clause (excluding the HAVING itself). Passing null will cause
+     *            all row groups to be included, and is required when row
+     *            grouping is not being used.
+     * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause
+     *            (excluding the ORDER BY itself). Passing null will use the
+     *            default sort order, which may be unordered.
+     * @param limit Limits the number of rows returned by the query,
+     *            formatted as LIMIT clause. Passing null denotes no LIMIT clause.
+     * @return A {@link Cursor} object, which is positioned before the first entry. Note that
+     * {@link Cursor}s are not synchronized, see the documentation for more details.
+     * @see Cursor
+     */
+    public Cursor query(boolean distinct, String table, String[] columns,
+            String selection, String[] selectionArgs, String groupBy,
+            String having, String orderBy, String limit) {
+        return queryWithFactory(null, distinct, table, columns, selection, selectionArgs,
+                groupBy, having, orderBy, limit, null);
+    }
+
+    /**
+     * 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.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * If the operation is canceled, then {@link OperationCanceledException} will be thrown
+     * when the query is executed.
+     * @return A {@link Cursor} object, which is positioned before the first entry. Note that
+     * {@link Cursor}s are not synchronized, see the documentation for more details.
+     * @see Cursor
+     */
+    public Cursor query(boolean distinct, String table, String[] columns,
+            String selection, String[] selectionArgs, String groupBy,
+            String having, String orderBy, String limit, CancellationSignal cancellationSignal) {
+        return queryWithFactory(null, distinct, table, columns, selection, selectionArgs,
+                groupBy, having, orderBy, limit, cancellationSignal);
+    }
+
+    /**
+     * 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) {
+        return queryWithFactory(cursorFactory, distinct, table, columns, selection,
+                selectionArgs, groupBy, having, orderBy, limit, null);
+    }
+
+    /**
+     * 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.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * If the operation is canceled, then {@link OperationCanceledException} will be thrown
+     * when the query is executed.
+     * @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, CancellationSignal cancellationSignal) {
+        acquireReference();
+        try {
+            String sql = SQLiteQueryBuilder.buildQueryString(
+                    distinct, table, columns, selection, groupBy, having, orderBy, limit);
+
+            return rawQueryWithFactory(cursorFactory, sql, selectionArgs,
+                    findEditTable(table), cancellationSignal);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Query the given table, returning a {@link Cursor} over the result set.
+     *
+     * @param table The table name to compile the query against.
+     * @param columns A list of which columns to return. Passing null will
+     *            return all columns, which is discouraged to prevent reading
+     *            data from storage that isn't going to be used.
+     * @param selection A filter declaring which rows to return, formatted as an
+     *            SQL WHERE clause (excluding the WHERE itself). Passing null
+     *            will return all rows for the given table.
+     * @param selectionArgs You may include ?s in selection, which will be
+     *         replaced by the values from selectionArgs, in order that they
+     *         appear in the selection. The values will be bound as Strings.
+     * @param groupBy A filter declaring how to group rows, formatted as an SQL
+     *            GROUP BY clause (excluding the GROUP BY itself). Passing null
+     *            will cause the rows to not be grouped.
+     * @param having A filter declare which row groups to include in the cursor,
+     *            if row grouping is being used, formatted as an SQL HAVING
+     *            clause (excluding the HAVING itself). Passing null will cause
+     *            all row groups to be included, and is required when row
+     *            grouping is not being used.
+     * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause
+     *            (excluding the ORDER BY itself). Passing null will use the
+     *            default sort order, which may be unordered.
+     * @return A {@link Cursor} object, which is positioned before the first entry. Note that
+     * {@link Cursor}s are not synchronized, see the documentation for more details.
+     * @see Cursor
+     */
+    public Cursor query(String table, String[] columns, String selection,
+            String[] selectionArgs, String groupBy, String having,
+            String orderBy) {
+
+        return query(false, table, columns, selection, selectionArgs, groupBy,
+                having, orderBy, null /* limit */);
+    }
+
+    /**
+     * Query the given table, returning a {@link Cursor} over the result set.
+     *
+     * @param table The table name to compile the query against.
+     * @param columns A list of which columns to return. Passing null will
+     *            return all columns, which is discouraged to prevent reading
+     *            data from storage that isn't going to be used.
+     * @param selection A filter declaring which rows to return, formatted as an
+     *            SQL WHERE clause (excluding the WHERE itself). Passing null
+     *            will return all rows for the given table.
+     * @param selectionArgs You may include ?s in selection, which will be
+     *         replaced by the values from selectionArgs, in order that they
+     *         appear in the selection. The values will be bound as Strings.
+     * @param groupBy A filter declaring how to group rows, formatted as an SQL
+     *            GROUP BY clause (excluding the GROUP BY itself). Passing null
+     *            will cause the rows to not be grouped.
+     * @param having A filter declare which row groups to include in the cursor,
+     *            if row grouping is being used, formatted as an SQL HAVING
+     *            clause (excluding the HAVING itself). Passing null will cause
+     *            all row groups to be included, and is required when row
+     *            grouping is not being used.
+     * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause
+     *            (excluding the ORDER BY itself). Passing null will use the
+     *            default sort order, which may be unordered.
+     * @param limit Limits the number of rows returned by the query,
+     *            formatted as LIMIT clause. Passing null denotes no LIMIT clause.
+     * @return A {@link Cursor} object, which is positioned before the first entry. Note that
+     * {@link Cursor}s are not synchronized, see the documentation for more details.
+     * @see Cursor
+     */
+    public Cursor query(String table, String[] columns, String selection,
+            String[] selectionArgs, String groupBy, String having,
+            String orderBy, String limit) {
+
+        return query(false, table, columns, selection, selectionArgs, groupBy,
+                having, orderBy, limit);
+    }
+
+    /**
+     * Runs the provided SQL and returns a {@link Cursor} over the result set.
+     *
+     * @param sql the SQL query. The SQL string must not be ; terminated
+     * @param selectionArgs You may include ?s in where clause in the query,
+     *     which will be replaced by the values from selectionArgs. The
+     *     values will be bound as Strings.
+     * @return A {@link Cursor} object, which is positioned before the first entry. Note that
+     * {@link Cursor}s are not synchronized, see the documentation for more details.
+     */
+    public Cursor rawQuery(String sql, String[] selectionArgs) {
+        return rawQueryWithFactory(null, sql, selectionArgs, null, null);
+    }
+
+    /**
+     * 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.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * If the operation is canceled, then {@link OperationCanceledException} will be thrown
+     * when the query is executed.
+     * @return A {@link Cursor} object, which is positioned before the first entry. Note that
+     * {@link Cursor}s are not synchronized, see the documentation for more details.
+     */
+    public Cursor rawQuery(String sql, String[] selectionArgs,
+            CancellationSignal cancellationSignal) {
+        return rawQueryWithFactory(null, sql, selectionArgs, null, cancellationSignal);
+    }
+
+    /**
+     * Runs the provided SQL and returns a cursor over the result set.
+     *
+     * @param cursorFactory the cursor factory to use, or null for the default factory
+     * @param sql the SQL query. The SQL string must not be ; terminated
+     * @param selectionArgs You may include ?s in where clause in the query,
+     *     which will be replaced by the values from selectionArgs. The
+     *     values will be bound as Strings.
+     * @param editTable the name of the first table, which is editable
+     * @return A {@link Cursor} object, which is positioned before the first entry. Note that
+     * {@link Cursor}s are not synchronized, see the documentation for more details.
+     */
+    public Cursor rawQueryWithFactory(
+            CursorFactory cursorFactory, String sql, String[] selectionArgs,
+            String editTable) {
+        return rawQueryWithFactory(cursorFactory, sql, selectionArgs, editTable, null);
+    }
+
+    /**
+     * Runs the provided SQL and returns a cursor over the result set.
+     *
+     * @param cursorFactory the cursor factory to use, or null for the default factory
+     * @param sql the SQL query. The SQL string must not be ; terminated
+     * @param selectionArgs You may include ?s in where clause in the query,
+     *     which will be replaced by the values from selectionArgs. The
+     *     values will be bound as Strings.
+     * @param editTable the name of the first table, which is editable
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * If the operation is canceled, then {@link OperationCanceledException} will be thrown
+     * when the query is executed.
+     * @return A {@link Cursor} object, which is positioned before the first entry. Note that
+     * {@link Cursor}s are not synchronized, see the documentation for more details.
+     */
+    public Cursor rawQueryWithFactory(
+            CursorFactory cursorFactory, String sql, String[] selectionArgs,
+            String editTable, CancellationSignal cancellationSignal) {
+        acquireReference();
+        try {
+            SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable,
+                    cancellationSignal);
+            return driver.query(cursorFactory != null ? cursorFactory : mCursorFactory,
+                    selectionArgs);
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Convenience method for inserting a row into the database.
+     *
+     * @param table the table to insert the row into
+     * @param nullColumnHack optional; may be <code>null</code>.
+     *            SQL doesn't allow inserting a completely empty row without
+     *            naming at least one column name.  If your provided <code>values</code> is
+     *            empty, no column names are known and an empty row can't be inserted.
+     *            If not set to null, the <code>nullColumnHack</code> parameter
+     *            provides the name of nullable column name to explicitly insert a NULL into
+     *            in the case where your <code>values</code> is empty.
+     * @param values this map contains the initial column values for the
+     *            row. The keys should be the column names and the values the
+     *            column values
+     * @return the row ID of the newly inserted row, or -1 if an error occurred
+     */
+    public long insert(String table, String nullColumnHack, ContentValues values) {
+        try {
+            return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE);
+        } catch (SQLException e) {
+            Log.e(TAG, "Error inserting " + values, e);
+            return -1;
+        }
+    }
+
+    /**
+     * Convenience method for inserting a row into the database.
+     *
+     * @param table the table to insert the row into
+     * @param nullColumnHack optional; may be <code>null</code>.
+     *            SQL doesn't allow inserting a completely empty row without
+     *            naming at least one column name.  If your provided <code>values</code> is
+     *            empty, no column names are known and an empty row can't be inserted.
+     *            If not set to null, the <code>nullColumnHack</code> parameter
+     *            provides the name of nullable column name to explicitly insert a NULL into
+     *            in the case where your <code>values</code> is empty.
+     * @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.
+     * Inserts a new row if a row does not already exist.
+     *
+     * @param table the table in which to replace the row
+     * @param nullColumnHack optional; may be <code>null</code>.
+     *            SQL doesn't allow inserting a completely empty row without
+     *            naming at least one column name.  If your provided <code>initialValues</code> is
+     *            empty, no column names are known and an empty row can't be inserted.
+     *            If not set to null, the <code>nullColumnHack</code> parameter
+     *            provides the name of nullable column name to explicitly insert a NULL into
+     *            in the case where your <code>initialValues</code> is empty.
+     * @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.
+     * @return the row ID of the newly inserted row, or -1 if an error occurred
+     */
+    public long replace(String table, String nullColumnHack, ContentValues initialValues) {
+        try {
+            return insertWithOnConflict(table, nullColumnHack, initialValues,
+                    CONFLICT_REPLACE);
+        } catch (SQLException e) {
+            Log.e(TAG, "Error inserting " + initialValues, e);
+            return -1;
+        }
+    }
+
+    /**
+     * Convenience method for replacing a row in the database.
+     * Inserts a new row if a row does not already exist.
+     *
+     * @param table the table in which to replace the row
+     * @param nullColumnHack optional; may be <code>null</code>.
+     *            SQL doesn't allow inserting a completely empty row without
+     *            naming at least one column name.  If your provided <code>initialValues</code> is
+     *            empty, no column names are known and an empty row can't be inserted.
+     *            If not set to null, the <code>nullColumnHack</code> parameter
+     *            provides the name of nullable column name to explicitly insert a NULL into
+     *            in the case where your <code>initialValues</code> is empty.
+     * @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.
+     * @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 optional; may be <code>null</code>.
+     *            SQL doesn't allow inserting a completely empty row without
+     *            naming at least one column name.  If your provided <code>initialValues</code> is
+     *            empty, no column names are known and an empty row can't be inserted.
+     *            If not set to null, the <code>nullColumnHack</code> parameter
+     *            provides the name of nullable column name to explicitly insert a NULL into
+     *            in the case where your <code>initialValues</code> is empty.
+     * @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 <code>-1</code> if either the
+     *            input parameter <code>conflictAlgorithm</code> = {@link #CONFLICT_IGNORE}
+     *            or an error occurred.
+     */
+    public long insertWithOnConflict(String table, String nullColumnHack,
+            ContentValues initialValues, int conflictAlgorithm) {
+        acquireReference();
+        try {
+            StringBuilder sql = new StringBuilder();
+            sql.append("INSERT");
+            sql.append(CONFLICT_VALUES[conflictAlgorithm]);
+            sql.append(" INTO ");
+            sql.append(table);
+            sql.append('(');
+
+            Object[] bindArgs = null;
+            int size = (initialValues != null && !initialValues.isEmpty())
+                    ? initialValues.size() : 0;
+            if (size > 0) {
+                bindArgs = new Object[size];
+                int i = 0;
+                for (String colName : initialValues.keySet()) {
+                    sql.append((i > 0) ? "," : "");
+                    sql.append(colName);
+                    bindArgs[i++] = initialValues.get(colName);
+                }
+                sql.append(')');
+                sql.append(" VALUES (");
+                for (i = 0; i < size; i++) {
+                    sql.append((i > 0) ? ",?" : "?");
+                }
+            } else {
+                sql.append(nullColumnHack + ") VALUES (NULL");
+            }
+            sql.append(')');
+
+            SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs);
+            try {
+                return statement.executeInsert();
+            } finally {
+                statement.close();
+            }
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * 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.
+     * @param whereArgs You may include ?s in the where clause, which
+     *            will be replaced by the values from whereArgs. The values
+     *            will be bound as Strings.
+     * @return the number of rows affected if a whereClause is passed in, 0
+     *         otherwise. To remove all rows and get a count pass "1" as the
+     *         whereClause.
+     */
+    public int delete(String table, String whereClause, String[] whereArgs) {
+        acquireReference();
+        try {
+            SQLiteStatement statement =  new SQLiteStatement(this, "DELETE FROM " + table +
+                    (!TextUtils.isEmpty(whereClause) ? " WHERE " + whereClause : ""), whereArgs);
+            try {
+                return statement.executeUpdateDelete();
+            } finally {
+                statement.close();
+            }
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * 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 whereArgs You may include ?s in the where clause, which
+     *            will be replaced by the values from whereArgs. The values
+     *            will be bound as Strings.
+     * @return the number of rows affected
+     */
+    public int update(String table, ContentValues values, String whereClause, String[] whereArgs) {
+        return updateWithOnConflict(table, values, whereClause, whereArgs, CONFLICT_NONE);
+    }
+
+    /**
+     * Convenience method for updating rows in the database.
+     *
+     * @param table the table to update in
+     * @param values a map from column names to new column values. null is a
+     *            valid value that will be translated to NULL.
+     * @param whereClause the optional WHERE clause to apply when updating.
+     *            Passing null will update all rows.
+     * @param whereArgs You may include ?s in the where clause, which
+     *            will be replaced by the values from whereArgs. The values
+     *            will be bound as Strings.
+     * @param conflictAlgorithm for update conflict resolver
+     * @return the number of rows affected
+     */
+    public int updateWithOnConflict(String table, ContentValues values,
+            String whereClause, String[] whereArgs, int conflictAlgorithm) {
+        if (values == null || values.isEmpty()) {
+            throw new IllegalArgumentException("Empty values");
+        }
+
+        acquireReference();
+        try {
+            StringBuilder sql = new StringBuilder(120);
+            sql.append("UPDATE ");
+            sql.append(CONFLICT_VALUES[conflictAlgorithm]);
+            sql.append(table);
+            sql.append(" SET ");
+
+            // move all bind args to one array
+            int setValuesSize = values.size();
+            int bindArgsSize = (whereArgs == null) ? setValuesSize : (setValuesSize + whereArgs.length);
+            Object[] bindArgs = new Object[bindArgsSize];
+            int i = 0;
+            for (String colName : values.keySet()) {
+                sql.append((i > 0) ? "," : "");
+                sql.append(colName);
+                bindArgs[i++] = values.get(colName);
+                sql.append("=?");
+            }
+            if (whereArgs != null) {
+                for (i = setValuesSize; i < bindArgsSize; i++) {
+                    bindArgs[i] = whereArgs[i - setValuesSize];
+                }
+            }
+            if (!TextUtils.isEmpty(whereClause)) {
+                sql.append(" WHERE ");
+                sql.append(whereClause);
+            }
+
+            SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs);
+            try {
+                return statement.executeUpdateDelete();
+            } finally {
+                statement.close();
+            }
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Execute a single SQL statement that is NOT a SELECT
+     * or any other SQL statement that returns data.
+     * <p>
+     * It has no means to return any data (such as the number of affected rows).
+     * Instead, you're encouraged to use {@link #insert(String, String, ContentValues)},
+     * {@link #update(String, ContentValues, String, String[])}, et al, when possible.
+     * </p>
+     * <p>
+     * When using {@link #enableWriteAheadLogging()}, journal_mode is
+     * automatically managed by this class. So, do not set journal_mode
+     * using "PRAGMA journal_mode'<value>" statement if your app is using
+     * {@link #enableWriteAheadLogging()}
+     * </p>
+     * <p>
+     * Note that {@code PRAGMA} values which apply on a per-connection basis
+     * should <em>not</em> be configured using this method; you should instead
+     * use {@link #execPerConnectionSQL} to ensure that they are uniformly
+     * applied to all current and future connections.
+     * </p>
+     *
+     * @param sql the SQL statement to be executed. Multiple statements separated by semicolons are
+     * not supported.
+     * @throws SQLException if the SQL string is invalid
+     */
+    public void execSQL(String sql) throws SQLException {
+        executeSql(sql, null);
+    }
+
+    /**
+     * Execute a single SQL statement that is NOT a SELECT/INSERT/UPDATE/DELETE.
+     * <p>
+     * For INSERT statements, use any of the following instead.
+     * <ul>
+     *   <li>{@link #insert(String, String, ContentValues)}</li>
+     *   <li>{@link #insertOrThrow(String, String, ContentValues)}</li>
+     *   <li>{@link #insertWithOnConflict(String, String, ContentValues, int)}</li>
+     * </ul>
+     * <p>
+     * For UPDATE statements, use any of the following instead.
+     * <ul>
+     *   <li>{@link #update(String, ContentValues, String, String[])}</li>
+     *   <li>{@link #updateWithOnConflict(String, ContentValues, String, String[], int)}</li>
+     * </ul>
+     * <p>
+     * For DELETE statements, use any of the following instead.
+     * <ul>
+     *   <li>{@link #delete(String, String, String[])}</li>
+     * </ul>
+     * <p>
+     * For example, the following are good candidates for using this method:
+     * <ul>
+     *   <li>ALTER TABLE</li>
+     *   <li>CREATE or DROP table / trigger / view / index / virtual table</li>
+     *   <li>REINDEX</li>
+     *   <li>RELEASE</li>
+     *   <li>SAVEPOINT</li>
+     *   <li>PRAGMA that returns no data</li>
+     * </ul>
+     * </p>
+     * <p>
+     * When using {@link #enableWriteAheadLogging()}, journal_mode is
+     * automatically managed by this class. So, do not set journal_mode
+     * using "PRAGMA journal_mode'<value>" statement if your app is using
+     * {@link #enableWriteAheadLogging()}
+     * </p>
+     * <p>
+     * Note that {@code PRAGMA} values which apply on a per-connection basis
+     * should <em>not</em> be configured using this method; you should instead
+     * use {@link #execPerConnectionSQL} to ensure that they are uniformly
+     * applied to all current and future connections.
+     * </p>
+     *
+     * @param sql the SQL statement to be executed. Multiple statements separated by semicolons are
+     * not supported.
+     * @param bindArgs only byte[], String, Long and Double are supported in bindArgs.
+     * @throws SQLException if the SQL string is invalid
+     */
+    public void execSQL(String sql, Object[] bindArgs) throws SQLException {
+        if (bindArgs == null) {
+            throw new IllegalArgumentException("Empty bindArgs");
+        }
+        executeSql(sql, bindArgs);
+    }
+
+    /** {@hide} */
+    public int executeSql(String sql, Object[] bindArgs) throws SQLException {
+        acquireReference();
+        try {
+            final int statementType = DatabaseUtils.getSqlStatementType(sql);
+            if (statementType == DatabaseUtils.STATEMENT_ATTACH) {
+                boolean disableWal = false;
+                synchronized (mLock) {
+                    if (!mHasAttachedDbsLocked) {
+                        mHasAttachedDbsLocked = true;
+                        disableWal = true;
+                        mConnectionPoolLocked.disableIdleConnectionHandler();
+                    }
+                }
+                if (disableWal) {
+                    disableWriteAheadLogging();
+                }
+            }
+
+            try (SQLiteStatement statement = new SQLiteStatement(this, sql, bindArgs)) {
+                return statement.executeUpdateDelete();
+            } finally {
+                // If schema was updated, close non-primary connections, otherwise they might
+                // have outdated schema information
+                if (statementType == DatabaseUtils.STATEMENT_DDL) {
+                    mConnectionPoolLocked.closeAvailableNonPrimaryConnectionsAndLogExceptions();
+                }
+            }
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Verifies that a SQL SELECT statement is valid by compiling it.
+     * If the SQL statement is not valid, this method will throw a {@link SQLiteException}.
+     *
+     * @param sql SQL to be validated
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * If the operation is canceled, then {@link OperationCanceledException} will be thrown
+     * when the query is executed.
+     * @throws SQLiteException if {@code sql} is invalid
+     */
+    public void validateSql(@NonNull String sql, @Nullable CancellationSignal cancellationSignal) {
+        getThreadSession().prepare(sql,
+                getThreadDefaultConnectionFlags(/* readOnly =*/ true), cancellationSignal, null);
+    }
+
+    /**
+     * Returns true if the database is opened as read only.
+     *
+     * @return True if database is opened as read only.
+     */
+    public boolean isReadOnly() {
+        synchronized (mLock) {
+            return isReadOnlyLocked();
+        }
+    }
+
+    private boolean isReadOnlyLocked() {
+        return (mConfigurationLocked.openFlags & OPEN_READ_MASK) == OPEN_READONLY;
+    }
+
+    /**
+     * Returns true if the database is in-memory db.
+     *
+     * @return True if the database is in-memory.
+     * @hide
+     */
+    public boolean isInMemoryDatabase() {
+        synchronized (mLock) {
+            return mConfigurationLocked.isInMemoryDb();
+        }
+    }
+
+    /**
+     * Returns true if the database is currently open.
+     *
+     * @return True if the database is currently open (has not been closed).
+     */
+    public boolean isOpen() {
+        synchronized (mLock) {
+            return mConnectionPoolLocked != null;
+        }
+    }
+
+    /**
+     * Returns true if the new version code is greater than the current database version.
+     *
+     * @param newVersion The new version code.
+     * @return True if the new version code is greater than the current database version.
+     */
+    public boolean needUpgrade(int newVersion) {
+        return newVersion > getVersion();
+    }
+
+    /**
+     * Gets the path to the database file.
+     *
+     * @return The path to the database file.
+     */
+    public final String getPath() {
+        synchronized (mLock) {
+            return mConfigurationLocked.path;
+        }
+    }
+
+    /**
+     * Sets the locale for this database.  Does nothing if this database has
+     * the {@link #NO_LOCALIZED_COLLATORS} flag set or was opened read only.
+     *
+     * @param locale The new locale.
+     *
+     * @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) {
+        if (locale == null) {
+            throw new IllegalArgumentException("locale must not be null.");
+        }
+
+        synchronized (mLock) {
+            throwIfNotOpenLocked();
+
+            final Locale oldLocale = mConfigurationLocked.locale;
+            mConfigurationLocked.locale = locale;
+            try {
+                mConnectionPoolLocked.reconfigure(mConfigurationLocked);
+            } catch (RuntimeException ex) {
+                mConfigurationLocked.locale = oldLocale;
+                throw ex;
+            }
+        }
+    }
+
+    /**
+     * Sets the maximum size of the prepared-statement cache for this database.
+     * (size of the cache = number of compiled-sql-statements stored in the cache).
+     *<p>
+     * Maximum cache size can ONLY be increased from its current size (default = 10).
+     * If this method is called with smaller size than the current maximum value,
+     * then IllegalStateException is thrown.
+     *<p>
+     * This method is thread-safe.
+     *
+     * @param cacheSize the size of the cache. can be (0 to {@link #MAX_SQL_CACHE_SIZE})
+     * @throws IllegalStateException if input cacheSize > {@link #MAX_SQL_CACHE_SIZE}.
+     */
+    public void setMaxSqlCacheSize(int cacheSize) {
+        if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) {
+            throw new IllegalStateException(
+                    "expected value between 0 and " + MAX_SQL_CACHE_SIZE);
+        }
+
+        synchronized (mLock) {
+            throwIfNotOpenLocked();
+
+            final int oldMaxSqlCacheSize = mConfigurationLocked.maxSqlCacheSize;
+            mConfigurationLocked.maxSqlCacheSize = cacheSize;
+            try {
+                mConnectionPoolLocked.reconfigure(mConfigurationLocked);
+            } catch (RuntimeException ex) {
+                mConfigurationLocked.maxSqlCacheSize = oldMaxSqlCacheSize;
+                throw ex;
+            }
+        }
+    }
+
+    /**
+     * Sets whether foreign key constraints are enabled for the database.
+     * <p>
+     * By default, foreign key constraints are not enforced by the database.
+     * This method allows an application to enable foreign key constraints.
+     * It must be called each time the database is opened to ensure that foreign
+     * key constraints are enabled for the session.
+     * </p><p>
+     * A good time to call this method is right after calling {@link #openOrCreateDatabase}
+     * or in the {@link SQLiteOpenHelper#onConfigure} callback.
+     * </p><p>
+     * When foreign key constraints are disabled, the database does not check whether
+     * changes to the database will violate foreign key constraints.  Likewise, when
+     * foreign key constraints are disabled, the database will not execute cascade
+     * delete or update triggers.  As a result, it is possible for the database
+     * state to become inconsistent.  To perform a database integrity check,
+     * call {@link #isDatabaseIntegrityOk}.
+     * </p><p>
+     * This method must not be called while a transaction is in progress.
+     * </p><p>
+     * See also <a href="http://sqlite.org/foreignkeys.html">SQLite Foreign Key Constraints</a>
+     * for more details about foreign key constraint support.
+     * </p>
+     *
+     * @param enable True to enable foreign key constraints, false to disable them.
+     *
+     * @throws IllegalStateException if the are transactions is in progress
+     * when this method is called.
+     */
+    public void setForeignKeyConstraintsEnabled(boolean enable) {
+        synchronized (mLock) {
+            throwIfNotOpenLocked();
+
+            if (mConfigurationLocked.foreignKeyConstraintsEnabled == enable) {
+                return;
+            }
+
+            mConfigurationLocked.foreignKeyConstraintsEnabled = enable;
+            try {
+                mConnectionPoolLocked.reconfigure(mConfigurationLocked);
+            } catch (RuntimeException ex) {
+                mConfigurationLocked.foreignKeyConstraintsEnabled = !enable;
+                throw ex;
+            }
+        }
+    }
+
+    /**
+     * This method enables parallel execution of queries from multiple threads on the
+     * same database.  It does this by opening multiple connections to the database
+     * and using a different database connection for each query.  The database
+     * journal mode is also changed to enable writes to proceed concurrently with reads.
+     * <p>
+     * When write-ahead logging is not enabled (the default), it is not possible for
+     * reads and writes to occur on the database at the same time.  Before modifying the
+     * database, the writer implicitly acquires an exclusive lock on the database which
+     * prevents readers from accessing the database until the write is completed.
+     * </p><p>
+     * In contrast, when write-ahead logging is enabled (by calling this method), write
+     * operations occur in a separate log file which allows reads to proceed concurrently.
+     * While a write is in progress, readers on other threads will perceive the state
+     * of the database as it was before the write began.  When the write completes, readers
+     * on other threads will then perceive the new state of the database.
+     * </p><p>
+     * It is a good idea to enable write-ahead logging whenever a database will be
+     * concurrently accessed and modified by multiple threads at the same time.
+     * However, write-ahead logging uses significantly more memory than ordinary
+     * journaling because there are multiple connections to the same database.
+     * So if a database will only be used by a single thread, or if optimizing
+     * concurrency is not very important, then write-ahead logging should be disabled.
+     * </p><p>
+     * After calling this method, execution of queries in parallel is enabled as long as
+     * the database remains open.  To disable execution of queries in parallel, either
+     * call {@link #disableWriteAheadLogging} or close the database and reopen it.
+     * </p><p>
+     * The maximum number of connections used to execute queries in parallel is
+     * dependent upon the device memory and possibly other properties.
+     * </p><p>
+     * If a query is part of a transaction, then it is executed on the same database handle the
+     * transaction was begun.
+     * </p><p>
+     * Writers should use {@link #beginTransactionNonExclusive()} or
+     * {@link #beginTransactionWithListenerNonExclusive(SQLiteTransactionListener)}
+     * to start a transaction.  Non-exclusive mode allows database file to be in readable
+     * by other threads executing queries.
+     * </p><p>
+     * If the database has any attached databases, then execution of queries in parallel is NOT
+     * possible.  Likewise, write-ahead logging is not supported for read-only databases
+     * or memory databases.  In such cases, {@link #enableWriteAheadLogging} returns false.
+     * </p><p>
+     * The best way to enable write-ahead logging is to pass the
+     * {@link #ENABLE_WRITE_AHEAD_LOGGING} flag to {@link #openDatabase}.  This is
+     * more efficient than calling {@link #enableWriteAheadLogging}.
+     * <code><pre>
+     *     SQLiteDatabase db = SQLiteDatabase.openDatabase("db_filename", cursorFactory,
+     *             SQLiteDatabase.CREATE_IF_NECESSARY | SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING,
+     *             myDatabaseErrorHandler);
+     * </pre></code>
+     * </p><p>
+     * Another way to enable write-ahead logging is to call {@link #enableWriteAheadLogging}
+     * after opening the database.
+     * <code><pre>
+     *     SQLiteDatabase db = SQLiteDatabase.openDatabase("db_filename", cursorFactory,
+     *             SQLiteDatabase.CREATE_IF_NECESSARY, myDatabaseErrorHandler);
+     *     db.enableWriteAheadLogging();
+     * </pre></code>
+     * </p><p>
+     * See also <a href="http://sqlite.org/wal.html">SQLite Write-Ahead Logging</a> for
+     * more details about how write-ahead logging works.
+     * </p>
+     *
+     * @return True if write-ahead logging is enabled.
+     *
+     * @throws IllegalStateException if there are transactions in progress at the
+     * time this method is called.  WAL mode can only be changed when there are no
+     * transactions in progress.
+     *
+     * @see #ENABLE_WRITE_AHEAD_LOGGING
+     * @see #disableWriteAheadLogging
+     */
+    public boolean enableWriteAheadLogging() {
+        synchronized (mLock) {
+            throwIfNotOpenLocked();
+
+            if ((mConfigurationLocked.openFlags & ENABLE_WRITE_AHEAD_LOGGING) != 0) {
+                return true;
+            }
+
+            if (isReadOnlyLocked()) {
+                // WAL doesn't make sense for readonly-databases.
+                // TODO: True, but connection pooling does still make sense...
+                return false;
+            }
+
+            if (mConfigurationLocked.isInMemoryDb()) {
+                Log.i(TAG, "can't enable WAL for memory databases.");
+                return false;
+            }
+
+            // make sure this database has NO attached databases because sqlite's write-ahead-logging
+            // doesn't work for databases with attached databases
+            if (mHasAttachedDbsLocked) {
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "this database: " + mConfigurationLocked.label
+                            + " has attached databases. can't  enable WAL.");
+                }
+                return false;
+            }
+
+            mConfigurationLocked.openFlags |= ENABLE_WRITE_AHEAD_LOGGING;
+            try {
+                mConnectionPoolLocked.reconfigure(mConfigurationLocked);
+            } catch (RuntimeException ex) {
+                mConfigurationLocked.openFlags &= ~ENABLE_WRITE_AHEAD_LOGGING;
+                throw ex;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * This method disables the features enabled by {@link #enableWriteAheadLogging()}.
+     *
+     * @throws IllegalStateException if there are transactions in progress at the
+     * time this method is called.  WAL mode can only be changed when there are no
+     * transactions in progress.
+     *
+     * @see #enableWriteAheadLogging
+     */
+    public void disableWriteAheadLogging() {
+        synchronized (mLock) {
+            throwIfNotOpenLocked();
+
+            final int oldFlags = mConfigurationLocked.openFlags;
+            final boolean walEnabled = (oldFlags & ENABLE_WRITE_AHEAD_LOGGING) != 0;
+            final boolean compatibilityWalEnabled =
+                    (oldFlags & ENABLE_LEGACY_COMPATIBILITY_WAL) != 0;
+            // WAL was never enabled for this database, so there's nothing left to do.
+            if (!walEnabled && !compatibilityWalEnabled) {
+                return;
+            }
+
+            // If an app explicitly disables WAL, it takes priority over any directive
+            // to use the legacy "compatibility WAL" mode.
+            mConfigurationLocked.openFlags &= ~ENABLE_WRITE_AHEAD_LOGGING;
+            mConfigurationLocked.openFlags &= ~ENABLE_LEGACY_COMPATIBILITY_WAL;
+
+            try {
+                mConnectionPoolLocked.reconfigure(mConfigurationLocked);
+            } catch (RuntimeException ex) {
+                mConfigurationLocked.openFlags = oldFlags;
+                throw ex;
+            }
+        }
+    }
+
+    /**
+     * Returns true if write-ahead logging has been enabled for this database.
+     *
+     * @return True if write-ahead logging has been enabled for this database.
+     *
+     * @see #enableWriteAheadLogging
+     * @see #ENABLE_WRITE_AHEAD_LOGGING
+     */
+    public boolean isWriteAheadLoggingEnabled() {
+        synchronized (mLock) {
+            throwIfNotOpenLocked();
+
+            return (mConfigurationLocked.openFlags & ENABLE_WRITE_AHEAD_LOGGING) != 0;
+        }
+    }
+
+    /**
+     * Collect statistics about all open databases in the current process.
+     * Used by bug report.
+     */
+    static ArrayList<DbStats> getDbStats() {
+        ArrayList<DbStats> dbStatsList = new ArrayList<DbStats>();
+        for (SQLiteDatabase db : getActiveDatabases()) {
+            db.collectDbStats(dbStatsList);
+        }
+        return dbStatsList;
+    }
+
+    @UnsupportedAppUsage
+    private void collectDbStats(ArrayList<DbStats> dbStatsList) {
+        synchronized (mLock) {
+            if (mConnectionPoolLocked != null) {
+                mConnectionPoolLocked.collectDbStats(dbStatsList);
+            }
+        }
+    }
+
+    @UnsupportedAppUsage
+    private static ArrayList<SQLiteDatabase> getActiveDatabases() {
+        ArrayList<SQLiteDatabase> databases = new ArrayList<SQLiteDatabase>();
+        synchronized (sActiveDatabases) {
+            databases.addAll(sActiveDatabases.keySet());
+        }
+        return databases;
+    }
+
+    /**
+     * Dump detailed information about all open databases in the current process.
+     * Used by bug report.
+     */
+    static void dumpAll(Printer printer, boolean verbose, boolean isSystem) {
+        // Use this ArraySet to collect file paths.
+        final ArraySet<String> directories = new ArraySet<>();
+
+        for (SQLiteDatabase db : getActiveDatabases()) {
+            db.dump(printer, verbose, isSystem, directories);
+        }
+
+        // Dump DB files in the directories.
+        if (directories.size() > 0) {
+            final String[] dirs = directories.toArray(new String[directories.size()]);
+            Arrays.sort(dirs);
+            for (String dir : dirs) {
+                dumpDatabaseDirectory(printer, new File(dir), isSystem);
+            }
+        }
+    }
+
+    private void dump(Printer printer, boolean verbose, boolean isSystem, ArraySet directories) {
+        synchronized (mLock) {
+            if (mConnectionPoolLocked != null) {
+                printer.println("");
+                mConnectionPoolLocked.dump(printer, verbose, directories);
+            }
+        }
+    }
+
+    private static void dumpDatabaseDirectory(Printer pw, File dir, boolean isSystem) {
+        pw.println("");
+        pw.println("Database files in " + dir.getAbsolutePath() + ":");
+        final File[] files = dir.listFiles();
+        if (files == null || files.length == 0) {
+            pw.println("  [none]");
+            return;
+        }
+        Arrays.sort(files, (a, b) -> a.getName().compareTo(b.getName()));
+
+        for (File f : files) {
+            if (isSystem) {
+                // If called within the system server, the directory contains other files too, so
+                // filter by file extensions.
+                // (If it's an app, just print all files because they may not use *.db
+                // extension.)
+                final String name = f.getName();
+                if (!(name.endsWith(".db") || name.endsWith(".db-wal")
+                        || name.endsWith(".db-journal")
+                        || name.endsWith(SQLiteGlobal.WIPE_CHECK_FILE_SUFFIX))) {
+                    continue;
+                }
+            }
+            pw.println(String.format("  %-40s %7db %s", f.getName(), f.length(),
+                    SQLiteDatabase.getFileTimestamps(f.getAbsolutePath())));
+        }
+    }
+
+    /**
+     * Returns list of full pathnames of all attached databases including the main database
+     * by executing 'pragma database_list' on the database.
+     *
+     * @return ArrayList of pairs of (database name, database file path) or null if the database
+     * is not open.
+     */
+    public List<Pair<String, String>> getAttachedDbs() {
+        ArrayList<Pair<String, String>> attachedDbs = new ArrayList<Pair<String, String>>();
+        synchronized (mLock) {
+            if (mConnectionPoolLocked == null) {
+                return null; // not open
+            }
+
+            if (!mHasAttachedDbsLocked) {
+                // No attached databases.
+                // There is a small window where attached databases exist but this flag is not
+                // set yet.  This can occur when this thread is in a race condition with another
+                // thread that is executing the SQL statement: "attach database <blah> as <foo>"
+                // If this thread is NOT ok with such a race condition (and thus possibly not
+                // receivethe entire list of attached databases), then the caller should ensure
+                // that no thread is executing any SQL statements while a thread is calling this
+                // method.  Typically, this method is called when 'adb bugreport' is done or the
+                // caller wants to collect stats on the database and all its attached databases.
+                attachedDbs.add(new Pair<String, String>("main", mConfigurationLocked.path));
+                return attachedDbs;
+            }
+
+            acquireReference();
+        }
+
+        try {
+            // has attached databases. query sqlite to get the list of attached databases.
+            Cursor c = null;
+            try {
+                c = rawQuery("pragma database_list;", null);
+                while (c.moveToNext()) {
+                    // sqlite returns a row for each database in the returned list of databases.
+                    //   in each row,
+                    //       1st column is the database name such as main, or the database
+                    //                              name specified on the "ATTACH" command
+                    //       2nd column is the database file path.
+                    attachedDbs.add(new Pair<String, String>(c.getString(1), c.getString(2)));
+                }
+            } finally {
+                if (c != null) {
+                    c.close();
+                }
+            }
+            return attachedDbs;
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Runs 'pragma integrity_check' on the given database (and all the attached databases)
+     * and returns true if the given database (and all its attached databases) pass integrity_check,
+     * false otherwise.
+     *<p>
+     * If the result is false, then this method logs the errors reported by the integrity_check
+     * command execution.
+     *<p>
+     * Note that 'pragma integrity_check' on a database can take a long time.
+     *
+     * @return true if the given database (and all its attached databases) pass integrity_check,
+     * false otherwise.
+     */
+    public boolean isDatabaseIntegrityOk() {
+        acquireReference();
+        try {
+            List<Pair<String, String>> attachedDbs = null;
+            try {
+                attachedDbs = getAttachedDbs();
+                if (attachedDbs == null) {
+                    throw new IllegalStateException("databaselist for: " + getPath() + " couldn't " +
+                            "be retrieved. probably because the database is closed");
+                }
+            } catch (SQLiteException e) {
+                // can't get attachedDb list. do integrity check on the main database
+                attachedDbs = new ArrayList<Pair<String, String>>();
+                attachedDbs.add(new Pair<String, String>("main", getPath()));
+            }
+
+            for (int i = 0; i < attachedDbs.size(); i++) {
+                Pair<String, String> p = attachedDbs.get(i);
+                SQLiteStatement prog = null;
+                try {
+                    prog = compileStatement("PRAGMA " + p.first + ".integrity_check(1);");
+                    String rslt = prog.simpleQueryForString();
+                    if (!rslt.equalsIgnoreCase("ok")) {
+                        // integrity_checker failed on main or attached databases
+                        Log.e(TAG, "PRAGMA integrity_check on " + p.second + " returned: " + rslt);
+                        return false;
+                    }
+                } finally {
+                    if (prog != null) prog.close();
+                }
+            }
+        } finally {
+            releaseReference();
+        }
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "SQLiteDatabase: " + getPath();
+    }
+
+    private void throwIfNotOpenLocked() {
+        if (mConnectionPoolLocked == null) {
+            throw new IllegalStateException("The database '" + mConfigurationLocked.label
+                    + "' is not open.");
+        }
+    }
+
+    /**
+     * Used to allow returning sub-classes of {@link Cursor} when calling query.
+     */
+    public interface CursorFactory {
+        /**
+         * See {@link SQLiteCursor#SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)}.
+         */
+        public Cursor newCursor(SQLiteDatabase db,
+                SQLiteCursorDriver masterQuery, String editTable,
+                SQLiteQuery query);
+    }
+
+    /**
+     * A callback interface for a custom sqlite3 function.
+     * This can be used to create a function that can be called from
+     * sqlite3 database triggers.
+     * @hide
+     */
+    public interface CustomFunction {
+        public void callback(String[] args);
+    }
+
+    /**
+     * Wrapper for configuration parameters that are used for opening {@link SQLiteDatabase}
+     */
+    public static final class OpenParams {
+        private final int mOpenFlags;
+        private final CursorFactory mCursorFactory;
+        private final DatabaseErrorHandler mErrorHandler;
+        private final int mLookasideSlotSize;
+        private final int mLookasideSlotCount;
+        private final long mIdleConnectionTimeout;
+        private final String mJournalMode;
+        private final String mSyncMode;
+
+        private OpenParams(int openFlags, CursorFactory cursorFactory,
+                DatabaseErrorHandler errorHandler, int lookasideSlotSize, int lookasideSlotCount,
+                long idleConnectionTimeout, String journalMode, String syncMode) {
+            mOpenFlags = openFlags;
+            mCursorFactory = cursorFactory;
+            mErrorHandler = errorHandler;
+            mLookasideSlotSize = lookasideSlotSize;
+            mLookasideSlotCount = lookasideSlotCount;
+            mIdleConnectionTimeout = idleConnectionTimeout;
+            mJournalMode = journalMode;
+            mSyncMode = syncMode;
+        }
+
+        /**
+         * Returns size in bytes of each lookaside slot or -1 if not set.
+         *
+         * @see Builder#setLookasideConfig(int, int)
+         */
+        @IntRange(from = -1)
+        public int getLookasideSlotSize() {
+            return mLookasideSlotSize;
+        }
+
+        /**
+         * Returns total number of lookaside memory slots per database connection or -1 if not
+         * set.
+         *
+         * @see Builder#setLookasideConfig(int, int)
+         */
+        @IntRange(from = -1)
+        public int getLookasideSlotCount() {
+            return mLookasideSlotCount;
+        }
+
+        /**
+         * Returns flags to control database access mode. Default value is 0.
+         *
+         * @see Builder#setOpenFlags(int)
+         */
+        @DatabaseOpenFlags
+        public int getOpenFlags() {
+            return mOpenFlags;
+        }
+
+        /**
+         * Returns an optional factory class that is called to instantiate a cursor when query
+         * is called
+         *
+         * @see Builder#setCursorFactory(CursorFactory)
+         */
+        @Nullable
+        public CursorFactory getCursorFactory() {
+            return mCursorFactory;
+        }
+
+        /**
+         * Returns handler for database corruption errors
+         *
+         * @see Builder#setErrorHandler(DatabaseErrorHandler)
+         */
+        @Nullable
+        public DatabaseErrorHandler getErrorHandler() {
+            return mErrorHandler;
+        }
+
+        /**
+         * Returns maximum number of milliseconds that SQLite connection is allowed to be idle
+         * before it is closed and removed from the pool.
+         * <p>If the value isn't set, the timeout defaults to the system wide timeout
+         *
+         * @return timeout in milliseconds or -1 if the value wasn't set.
+         */
+        public long getIdleConnectionTimeout() {
+            return mIdleConnectionTimeout;
+        }
+
+        /**
+         * Returns <a href="https://sqlite.org/pragma.html#pragma_journal_mode">journal mode</a>.
+         * This journal mode will only be used if {@link SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING}
+         * flag is not set, otherwise a platform will use "WAL" journal mode.
+         * @see Builder#setJournalMode(String)
+         */
+        @Nullable
+        public String getJournalMode() {
+            return mJournalMode;
+        }
+
+        /**
+         * Returns <a href="https://sqlite.org/pragma.html#pragma_synchronous">synchronous mode</a>.
+         * If not set, a system wide default will be used.
+         * @see Builder#setSynchronousMode(String)
+         */
+        @Nullable
+        public String getSynchronousMode() {
+            return mSyncMode;
+        }
+
+        /**
+         * Creates a new instance of builder {@link Builder#Builder(OpenParams) initialized} with
+         * {@code this} parameters.
+         * @hide
+         */
+        @NonNull
+        public Builder toBuilder() {
+            return new Builder(this);
+        }
+
+        /**
+         * Builder for {@link OpenParams}.
+         */
+        public static final class Builder {
+            private int mLookasideSlotSize = -1;
+            private int mLookasideSlotCount = -1;
+            private long mIdleConnectionTimeout = -1;
+            private int mOpenFlags;
+            private CursorFactory mCursorFactory;
+            private DatabaseErrorHandler mErrorHandler;
+            private String mJournalMode;
+            private String mSyncMode;
+
+            public Builder() {
+            }
+
+            public Builder(OpenParams params) {
+                mLookasideSlotSize = params.mLookasideSlotSize;
+                mLookasideSlotCount = params.mLookasideSlotCount;
+                mOpenFlags = params.mOpenFlags;
+                mCursorFactory = params.mCursorFactory;
+                mErrorHandler = params.mErrorHandler;
+                mJournalMode = params.mJournalMode;
+                mSyncMode = params.mSyncMode;
+            }
+
+            /**
+             * Configures
+             * <a href="https://sqlite.org/malloc.html#lookaside">lookaside memory allocator</a>
+             *
+             * <p>SQLite default settings will be used, if this method isn't called.
+             * Use {@code setLookasideConfig(0,0)} to disable lookaside
+             *
+             * <p><strong>Note:</strong> Provided slotSize/slotCount configuration is just a
+             * recommendation. The system may choose different values depending on a device, e.g.
+             * lookaside allocations can be disabled on low-RAM devices
+             *
+             * @param slotSize The size in bytes of each lookaside slot.
+             * @param slotCount The total number of lookaside memory slots per database connection.
+             */
+            public Builder setLookasideConfig(@IntRange(from = 0) final int slotSize,
+                    @IntRange(from = 0) final int slotCount) {
+                Preconditions.checkArgument(slotSize >= 0,
+                        "lookasideSlotCount cannot be negative");
+                Preconditions.checkArgument(slotCount >= 0,
+                        "lookasideSlotSize cannot be negative");
+                Preconditions.checkArgument(
+                        (slotSize > 0 && slotCount > 0) || (slotCount == 0 && slotSize == 0),
+                        "Invalid configuration: " + slotSize + ", " + slotCount);
+
+                mLookasideSlotSize = slotSize;
+                mLookasideSlotCount = slotCount;
+                return this;
+            }
+
+            /**
+             * Returns true if {@link #ENABLE_WRITE_AHEAD_LOGGING} flag is set
+             * @hide
+             */
+            public boolean isWriteAheadLoggingEnabled() {
+                return (mOpenFlags & ENABLE_WRITE_AHEAD_LOGGING) != 0;
+            }
+
+            /**
+             * Sets flags to control database access mode
+             * @param openFlags The new flags to set
+             * @see #OPEN_READWRITE
+             * @see #OPEN_READONLY
+             * @see #CREATE_IF_NECESSARY
+             * @see #NO_LOCALIZED_COLLATORS
+             * @see #ENABLE_WRITE_AHEAD_LOGGING
+             * @return same builder instance for chaining multiple calls into a single statement
+             */
+            @NonNull
+            public Builder setOpenFlags(@DatabaseOpenFlags int openFlags) {
+                mOpenFlags = openFlags;
+                return this;
+            }
+
+            /**
+             * Adds flags to control database access mode
+             *
+             * @param openFlags The new flags to add
+             * @return same builder instance for chaining multiple calls into a single statement
+             */
+            @NonNull
+            public Builder addOpenFlags(@DatabaseOpenFlags int openFlags) {
+                mOpenFlags |= openFlags;
+                return this;
+            }
+
+            /**
+             * Removes database access mode flags
+             *
+             * @param openFlags Flags to remove
+             * @return same builder instance for chaining multiple calls into a single statement
+             */
+            @NonNull
+            public Builder removeOpenFlags(@DatabaseOpenFlags int openFlags) {
+                mOpenFlags &= ~openFlags;
+                return this;
+            }
+
+            /**
+             * Sets {@link #ENABLE_WRITE_AHEAD_LOGGING} flag if {@code enabled} is {@code true},
+             * unsets otherwise
+             * @hide
+             */
+            public void setWriteAheadLoggingEnabled(boolean enabled) {
+                if (enabled) {
+                    addOpenFlags(ENABLE_WRITE_AHEAD_LOGGING);
+                } else {
+                    removeOpenFlags(ENABLE_WRITE_AHEAD_LOGGING);
+                }
+            }
+
+            /**
+             * Set an optional factory class that is called to instantiate a cursor when query
+             * is called.
+             *
+             * @param cursorFactory instance
+             * @return same builder instance for chaining multiple calls into a single statement
+             */
+            @NonNull
+            public Builder setCursorFactory(@Nullable CursorFactory cursorFactory) {
+                mCursorFactory = cursorFactory;
+                return this;
+            }
+
+
+            /**
+             * Sets {@link DatabaseErrorHandler} object to handle db corruption errors
+             */
+            @NonNull
+            public Builder setErrorHandler(@Nullable DatabaseErrorHandler errorHandler) {
+                mErrorHandler = errorHandler;
+                return this;
+            }
+
+            /**
+             * Sets the maximum number of milliseconds that SQLite connection is allowed to be idle
+             * before it is closed and removed from the pool.
+             *
+             * <p><b>DO NOT USE</b> this method.
+             * This feature has negative side effects that are very hard to foresee.
+             * <p>A connection timeout allows the system to internally close a connection to
+             * a SQLite database after a given timeout, which is good for reducing app's memory
+             * consumption.
+             * <b>However</b> the side effect is it <b>will reset all of SQLite's per-connection
+             * states</b>, which are typically modified with a {@code PRAGMA} statement, and
+             * these states <b>will not be restored</b> when a connection is re-established
+             * internally, and the system does not provide a callback for an app to reconfigure a
+             * connection.
+             * This feature may only be used if an app relies on none of such per-connection states.
+             *
+             * @param idleConnectionTimeoutMs timeout in milliseconds. Use {@link Long#MAX_VALUE}
+             * to allow unlimited idle connections.
+             *
+             * @see SQLiteOpenHelper#setIdleConnectionTimeout(long)
+             *
+             * @deprecated DO NOT USE this method. See the javadoc for the details.
+             */
+            @NonNull
+            @Deprecated
+            public Builder setIdleConnectionTimeout(
+                    @IntRange(from = 0) long idleConnectionTimeoutMs) {
+                Preconditions.checkArgument(idleConnectionTimeoutMs >= 0,
+                        "idle connection timeout cannot be negative");
+                mIdleConnectionTimeout = idleConnectionTimeoutMs;
+                return this;
+            }
+
+
+            /**
+             * Sets <a href="https://sqlite.org/pragma.html#pragma_journal_mode">journal mode</a>
+             * to use when {@link SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING} flag is not set.
+             */
+            @NonNull
+            public Builder setJournalMode(@NonNull  String journalMode) {
+                Objects.requireNonNull(journalMode);
+                mJournalMode = journalMode;
+                return this;
+            }
+
+            /**w
+             * Sets <a href="https://sqlite.org/pragma.html#pragma_synchronous">synchronous mode</a>
+             * .
+             * @return
+             */
+            @NonNull
+            public Builder setSynchronousMode(@NonNull String syncMode) {
+                Objects.requireNonNull(syncMode);
+                mSyncMode = syncMode;
+                return this;
+            }
+
+            /**
+             * Creates an instance of {@link OpenParams} with the options that were previously set
+             * on this builder
+             */
+            @NonNull
+            public OpenParams build() {
+                return new OpenParams(mOpenFlags, mCursorFactory, mErrorHandler, mLookasideSlotSize,
+                        mLookasideSlotCount, mIdleConnectionTimeout, mJournalMode, mSyncMode);
+            }
+        }
+    }
+
+    /** @hide */
+    @IntDef(flag = true, prefix = {"OPEN_", "CREATE_", "NO_", "ENABLE_"}, value = {
+            OPEN_READWRITE,
+            OPEN_READONLY,
+            CREATE_IF_NECESSARY,
+            NO_LOCALIZED_COLLATORS,
+            ENABLE_WRITE_AHEAD_LOGGING
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DatabaseOpenFlags {}
+
+    /** @hide */
+    public static void wipeDetected(String filename, String reason) {
+        wtfAsSystemServer(TAG, "DB wipe detected:"
+                + " package=" + ActivityThread.currentPackageName()
+                + " reason=" + reason
+                + " file=" + filename
+                + " " + getFileTimestamps(filename)
+                + " checkfile " + getFileTimestamps(filename + SQLiteGlobal.WIPE_CHECK_FILE_SUFFIX),
+                new Throwable("STACKTRACE"));
+    }
+
+    /** @hide */
+    public static String getFileTimestamps(String path) {
+        try {
+            BasicFileAttributes attr = Files.readAttributes(
+                    FileSystems.getDefault().getPath(path), BasicFileAttributes.class);
+            return "ctime=" + attr.creationTime()
+                    + " mtime=" + attr.lastModifiedTime()
+                    + " atime=" + attr.lastAccessTime();
+        } catch (IOException e) {
+            return "[unable to obtain timestamp]";
+        }
+    }
+
+    /** @hide */
+    static void wtfAsSystemServer(String tag, String message, Throwable stacktrace) {
+        Log.e(tag, message, stacktrace);
+        ContentResolver.onDbCorruption(tag, message, stacktrace);
+    }
+}
+
diff --git a/android/database/sqlite/SQLiteDatabaseConfiguration.java b/android/database/sqlite/SQLiteDatabaseConfiguration.java
new file mode 100644
index 0000000..21c21c9
--- /dev/null
+++ b/android/database/sqlite/SQLiteDatabaseConfiguration.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2011 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 android.database.sqlite;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.util.ArrayMap;
+import android.util.Pair;
+
+import java.util.ArrayList;
+import java.util.Locale;
+import java.util.function.BinaryOperator;
+import java.util.function.UnaryOperator;
+import java.util.regex.Pattern;
+
+/**
+ * Describes how to configure a database.
+ * <p>
+ * The purpose of this object is to keep track of all of the little
+ * configuration settings that are applied to a database after it
+ * is opened so that they can be applied to all connections in the
+ * connection pool uniformly.
+ * </p><p>
+ * Each connection maintains its own copy of this object so it can
+ * keep track of which settings have already been applied.
+ * </p>
+ *
+ * @hide
+ */
+public final class SQLiteDatabaseConfiguration {
+    // The pattern we use to strip email addresses from database paths
+    // when constructing a label to use in log messages.
+    private static final Pattern EMAIL_IN_DB_PATTERN =
+            Pattern.compile("[\\w\\.\\-]+@[\\w\\.\\-]+");
+
+    /**
+     * Special path used by in-memory databases.
+     */
+    public static final String MEMORY_DB_PATH = ":memory:";
+
+    /**
+     * The database path.
+     */
+    public final String path;
+
+    /**
+     * The label to use to describe the database when it appears in logs.
+     * This is derived from the path but is stripped to remove PII.
+     */
+    public final String label;
+
+    /**
+     * The flags used to open the database.
+     */
+    public int openFlags;
+
+    /**
+     * The maximum size of the prepared statement cache for each database connection.
+     * Must be non-negative.
+     *
+     * Default is 25.
+     */
+    @UnsupportedAppUsage
+    public int maxSqlCacheSize;
+
+    /**
+     * The database locale.
+     *
+     * Default is the value returned by {@link Locale#getDefault()}.
+     */
+    public Locale locale;
+
+    /**
+     * True if foreign key constraints are enabled.
+     *
+     * Default is false.
+     */
+    public boolean foreignKeyConstraintsEnabled;
+
+    /**
+     * The custom scalar functions to register.
+     */
+    public final ArrayMap<String, UnaryOperator<String>> customScalarFunctions
+            = new ArrayMap<>();
+
+    /**
+     * The custom aggregate functions to register.
+     */
+    public final ArrayMap<String, BinaryOperator<String>> customAggregateFunctions
+            = new ArrayMap<>();
+
+    /**
+     * The statements to execute to initialize each connection.
+     */
+    public final ArrayList<Pair<String, Object[]>> perConnectionSql = new ArrayList<>();
+
+    /**
+     * The size in bytes of each lookaside slot
+     *
+     * <p>If negative, the default lookaside configuration will be used
+     */
+    public int lookasideSlotSize = -1;
+
+    /**
+     * The total number of lookaside memory slots per database connection
+     *
+     * <p>If negative, the default lookaside configuration will be used
+     */
+    public int lookasideSlotCount = -1;
+
+    /**
+     * The number of milliseconds that SQLite connection is allowed to be idle before it
+     * is closed and removed from the pool.
+     * <p>By default, idle connections are not closed
+     */
+    public long idleConnectionTimeoutMs = Long.MAX_VALUE;
+
+    /**
+     * Journal mode to use when {@link SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING} is not set.
+     * <p>Default is returned by {@link SQLiteGlobal#getDefaultJournalMode()}
+     */
+    public String journalMode;
+
+    /**
+     * Synchronous mode to use.
+     * <p>Default is returned by {@link SQLiteGlobal#getDefaultSyncMode()}
+     * or {@link SQLiteGlobal#getWALSyncMode()} depending on journal mode
+     */
+    public String syncMode;
+
+    /**
+     * Creates a database configuration with the required parameters for opening a
+     * database and default values for all other parameters.
+     *
+     * @param path The database path.
+     * @param openFlags Open flags for the database, such as {@link SQLiteDatabase#OPEN_READWRITE}.
+     */
+    public SQLiteDatabaseConfiguration(String path, int openFlags) {
+        if (path == null) {
+            throw new IllegalArgumentException("path must not be null.");
+        }
+
+        this.path = path;
+        label = stripPathForLogs(path);
+        this.openFlags = openFlags;
+
+        // Set default values for optional parameters.
+        maxSqlCacheSize = 25;
+        locale = Locale.getDefault();
+    }
+
+    /**
+     * Creates a database configuration as a copy of another configuration.
+     *
+     * @param other The other configuration.
+     */
+    public SQLiteDatabaseConfiguration(SQLiteDatabaseConfiguration other) {
+        if (other == null) {
+            throw new IllegalArgumentException("other must not be null.");
+        }
+
+        this.path = other.path;
+        this.label = other.label;
+        updateParametersFrom(other);
+    }
+
+    /**
+     * Updates the non-immutable parameters of this configuration object
+     * from the other configuration object.
+     *
+     * @param other The object from which to copy the parameters.
+     */
+    public void updateParametersFrom(SQLiteDatabaseConfiguration other) {
+        if (other == null) {
+            throw new IllegalArgumentException("other must not be null.");
+        }
+        if (!path.equals(other.path)) {
+            throw new IllegalArgumentException("other configuration must refer to "
+                    + "the same database.");
+        }
+
+        openFlags = other.openFlags;
+        maxSqlCacheSize = other.maxSqlCacheSize;
+        locale = other.locale;
+        foreignKeyConstraintsEnabled = other.foreignKeyConstraintsEnabled;
+        customScalarFunctions.clear();
+        customScalarFunctions.putAll(other.customScalarFunctions);
+        customAggregateFunctions.clear();
+        customAggregateFunctions.putAll(other.customAggregateFunctions);
+        perConnectionSql.clear();
+        perConnectionSql.addAll(other.perConnectionSql);
+        lookasideSlotSize = other.lookasideSlotSize;
+        lookasideSlotCount = other.lookasideSlotCount;
+        idleConnectionTimeoutMs = other.idleConnectionTimeoutMs;
+        journalMode = other.journalMode;
+        syncMode = other.syncMode;
+    }
+
+    /**
+     * Returns true if the database is in-memory.
+     * @return True if the database is in-memory.
+     */
+    public boolean isInMemoryDb() {
+        return path.equalsIgnoreCase(MEMORY_DB_PATH);
+    }
+
+    boolean isLegacyCompatibilityWalEnabled() {
+        return journalMode == null && syncMode == null
+                && (openFlags & SQLiteDatabase.ENABLE_LEGACY_COMPATIBILITY_WAL) != 0;
+    }
+
+    private static String stripPathForLogs(String path) {
+        if (path.indexOf('@') == -1) {
+            return path;
+        }
+        return EMAIL_IN_DB_PATTERN.matcher(path).replaceAll("XX@YY");
+    }
+
+    boolean isLookasideConfigSet() {
+        return lookasideSlotCount >= 0 && lookasideSlotSize >= 0;
+    }
+}
diff --git a/android/database/sqlite/SQLiteDatabaseCorruptException.java b/android/database/sqlite/SQLiteDatabaseCorruptException.java
new file mode 100644
index 0000000..488ef46
--- /dev/null
+++ b/android/database/sqlite/SQLiteDatabaseCorruptException.java
@@ -0,0 +1,44 @@
+/*
+ * 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 android.database.sqlite;
+
+/**
+ * An exception that indicates that the SQLite database file is corrupt.
+ */
+public class SQLiteDatabaseCorruptException extends SQLiteException {
+    public SQLiteDatabaseCorruptException() {}
+
+    public SQLiteDatabaseCorruptException(String error) {
+        super(error);
+    }
+
+    /**
+     * @return true if a given {@link Throwable} or any of its inner causes is of
+     * {@link SQLiteDatabaseCorruptException}.
+     *
+     * @hide
+     */
+    public static boolean isCorruptException(Throwable th) {
+        while (th != null) {
+            if (th instanceof SQLiteDatabaseCorruptException) {
+                return true;
+            }
+            th = th.getCause();
+        }
+        return false;
+    }
+}
diff --git a/android/database/sqlite/SQLiteDatabaseLockedException.java b/android/database/sqlite/SQLiteDatabaseLockedException.java
new file mode 100644
index 0000000..f0e2d81
--- /dev/null
+++ b/android/database/sqlite/SQLiteDatabaseLockedException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2010 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 android.database.sqlite;
+
+/**
+ * Thrown if  the database engine was unable to acquire the
+ * database locks it needs to do its job.  If the statement is a [COMMIT]
+ * or occurs outside of an explicit transaction, then you can retry the
+ * statement.  If the statement is not a [COMMIT] and occurs within a
+ * explicit transaction then you should rollback the transaction before
+ * continuing.
+ */
+public class SQLiteDatabaseLockedException extends SQLiteException {
+    public SQLiteDatabaseLockedException() {}
+
+    public SQLiteDatabaseLockedException(String error) {
+        super(error);
+    }
+}
diff --git a/android/database/sqlite/SQLiteDatatypeMismatchException.java b/android/database/sqlite/SQLiteDatatypeMismatchException.java
new file mode 100644
index 0000000..7f82535
--- /dev/null
+++ b/android/database/sqlite/SQLiteDatatypeMismatchException.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2008 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 android.database.sqlite;
+
+public class SQLiteDatatypeMismatchException extends SQLiteException {
+    public SQLiteDatatypeMismatchException() {}
+
+    public SQLiteDatatypeMismatchException(String error) {
+        super(error);
+    }
+}
diff --git a/android/database/sqlite/SQLiteDebug.java b/android/database/sqlite/SQLiteDebug.java
new file mode 100644
index 0000000..165f863
--- /dev/null
+++ b/android/database/sqlite/SQLiteDebug.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2007 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 android.database.sqlite;
+
+import android.annotation.TestApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Process;
+import android.os.SystemProperties;
+import android.util.Log;
+import android.util.Printer;
+
+import java.util.ArrayList;
+
+/**
+ * Provides debugging info about all SQLite databases running in the current process.
+ *
+ * {@hide}
+ */
+@TestApi
+public final class SQLiteDebug {
+    private static native void nativeGetPagerStats(PagerStats stats);
+
+    /**
+     * Inner class to avoid getting the value frozen in zygote.
+     *
+     * {@hide}
+     */
+    public static final class NoPreloadHolder {
+        /**
+         * Controls the printing of informational SQL log messages.
+         *
+         * Enable using "adb shell setprop log.tag.SQLiteLog VERBOSE".
+         */
+        public static final boolean DEBUG_SQL_LOG =
+                Log.isLoggable("SQLiteLog", Log.VERBOSE);
+
+        /**
+         * Controls the printing of SQL statements as they are executed.
+         *
+         * Enable using "adb shell setprop log.tag.SQLiteStatements VERBOSE".
+         */
+        public static final boolean DEBUG_SQL_STATEMENTS =
+                Log.isLoggable("SQLiteStatements", Log.VERBOSE);
+
+        /**
+         * Controls the printing of wall-clock time taken to execute SQL statements
+         * as they are executed.
+         *
+         * Enable using "adb shell setprop log.tag.SQLiteTime VERBOSE".
+         */
+        public static final boolean DEBUG_SQL_TIME =
+                Log.isLoggable("SQLiteTime", Log.VERBOSE);
+
+
+        /**
+         * True to enable database performance testing instrumentation.
+         */
+        public static final boolean DEBUG_LOG_SLOW_QUERIES = Build.IS_DEBUGGABLE;
+
+        private static final String SLOW_QUERY_THRESHOLD_PROP = "db.log.slow_query_threshold";
+
+        private static final String SLOW_QUERY_THRESHOLD_UID_PROP =
+                SLOW_QUERY_THRESHOLD_PROP + "." + Process.myUid();
+
+        /**
+         * Whether to add detailed information to slow query log.
+         */
+        public static final boolean DEBUG_LOG_DETAILED = Build.IS_DEBUGGABLE
+                && SystemProperties.getBoolean("db.log.detailed", false);
+    }
+
+    private SQLiteDebug() {
+    }
+
+    /**
+     * Determines whether a query should be logged.
+     *
+     * Reads the "db.log.slow_query_threshold" system property, which can be changed
+     * by the user at any time.  If the value is zero, then all queries will
+     * be considered slow.  If the value does not exist or is negative, then no queries will
+     * be considered slow.
+     *
+     * To enable it for a specific UID, "db.log.slow_query_threshold.UID" could also be used.
+     *
+     * This value can be changed dynamically while the system is running.
+     * For example, "adb shell setprop db.log.slow_query_threshold 200" will
+     * log all queries that take 200ms or longer to run.
+     * @hide
+     */
+    public static boolean shouldLogSlowQuery(long elapsedTimeMillis) {
+        final int slowQueryMillis = Math.min(
+                SystemProperties.getInt(NoPreloadHolder.SLOW_QUERY_THRESHOLD_PROP,
+                        Integer.MAX_VALUE),
+                SystemProperties.getInt(NoPreloadHolder.SLOW_QUERY_THRESHOLD_UID_PROP,
+                        Integer.MAX_VALUE));
+        return elapsedTimeMillis >= slowQueryMillis;
+    }
+
+    /**
+     * Contains statistics about the active pagers in the current process.
+     *
+     * @see #nativeGetPagerStats(PagerStats)
+     */
+    public static class PagerStats {
+
+        @UnsupportedAppUsage
+        public PagerStats() {
+        }
+
+        /** the current amount of memory checked out by sqlite using sqlite3_malloc().
+         * documented at http://www.sqlite.org/c3ref/c_status_malloc_size.html
+         */
+        @UnsupportedAppUsage
+        public int memoryUsed;
+
+        /** the number of bytes of page cache allocation which could not be sattisfied by the
+         * SQLITE_CONFIG_PAGECACHE buffer and where forced to overflow to sqlite3_malloc().
+         * The returned value includes allocations that overflowed because they where too large
+         * (they were larger than the "sz" parameter to SQLITE_CONFIG_PAGECACHE) and allocations
+         * that overflowed because no space was left in the page cache.
+         * documented at http://www.sqlite.org/c3ref/c_status_malloc_size.html
+         */
+        @UnsupportedAppUsage
+        public int pageCacheOverflow;
+
+        /** records the largest memory allocation request handed to sqlite3.
+         * documented at http://www.sqlite.org/c3ref/c_status_malloc_size.html
+         */
+        @UnsupportedAppUsage
+        public int largestMemAlloc;
+
+        /** a list of {@link DbStats} - one for each main database opened by the applications
+         * running on the android device
+         */
+        @UnsupportedAppUsage
+        public ArrayList<DbStats> dbStats;
+    }
+
+    /**
+     * contains statistics about a database
+     */
+    public static class DbStats {
+        /** name of the database */
+        @UnsupportedAppUsage
+        public String dbName;
+
+        /** the page size for the database */
+        @UnsupportedAppUsage
+        public long pageSize;
+
+        /** the database size */
+        @UnsupportedAppUsage
+        public long dbSize;
+
+        /**
+         * Number of lookaside slots: http://www.sqlite.org/c3ref/c_dbstatus_lookaside_used.html */
+        @UnsupportedAppUsage
+        public int lookaside;
+
+        /** statement cache stats: hits/misses/cachesize */
+        public String cache;
+
+        public DbStats(String dbName, long pageCount, long pageSize, int lookaside,
+            int hits, int misses, int cachesize) {
+            this.dbName = dbName;
+            this.pageSize = pageSize / 1024;
+            dbSize = (pageCount * pageSize) / 1024;
+            this.lookaside = lookaside;
+            this.cache = hits + "/" + misses + "/" + cachesize;
+        }
+    }
+
+    /**
+     * return all pager and database stats for the current process.
+     * @return {@link PagerStats}
+     */
+    @UnsupportedAppUsage
+    public static PagerStats getDatabaseInfo() {
+        PagerStats stats = new PagerStats();
+        nativeGetPagerStats(stats);
+        stats.dbStats = SQLiteDatabase.getDbStats();
+        return stats;
+    }
+
+    /**
+     * Dumps detailed information about all databases used by the process.
+     * @param printer The printer for dumping database state.
+     * @param args Command-line arguments supplied to dumpsys dbinfo
+     */
+    public static void dump(Printer printer, String[] args) {
+        dump(printer, args, false);
+    }
+
+    /** @hide */
+    public static void dump(Printer printer, String[] args, boolean isSystem) {
+        boolean verbose = false;
+        for (String arg : args) {
+            if (arg.equals("-v")) {
+                verbose = true;
+            }
+        }
+
+        SQLiteDatabase.dumpAll(printer, verbose, isSystem);
+    }
+}
diff --git a/android/database/sqlite/SQLiteDirectCursorDriver.java b/android/database/sqlite/SQLiteDirectCursorDriver.java
new file mode 100644
index 0000000..1721e0c
--- /dev/null
+++ b/android/database/sqlite/SQLiteDirectCursorDriver.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2007 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 android.database.sqlite;
+
+import android.annotation.TestApi;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+import android.os.CancellationSignal;
+
+/**
+ * A cursor driver that uses the given query directly.
+ * 
+ * @hide
+ */
+@TestApi
+public final class SQLiteDirectCursorDriver implements SQLiteCursorDriver {
+    private final SQLiteDatabase mDatabase;
+    private final String mEditTable; 
+    private final String mSql;
+    private final CancellationSignal mCancellationSignal;
+    private SQLiteQuery mQuery;
+
+    public SQLiteDirectCursorDriver(SQLiteDatabase db, String sql, String editTable,
+            CancellationSignal cancellationSignal) {
+        mDatabase = db;
+        mEditTable = editTable;
+        mSql = sql;
+        mCancellationSignal = cancellationSignal;
+    }
+
+    public Cursor query(CursorFactory factory, String[] selectionArgs) {
+        final SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, mCancellationSignal);
+        final Cursor cursor;
+        try {
+            query.bindAllArgsAsStrings(selectionArgs);
+
+            if (factory == null) {
+                cursor = new SQLiteCursor(this, mEditTable, query);
+            } else {
+                cursor = factory.newCursor(mDatabase, this, mEditTable, query);
+            }
+        } catch (RuntimeException ex) {
+            query.close();
+            throw ex;
+        }
+
+        mQuery = query;
+        return cursor;
+    }
+
+    public void cursorClosed() {
+        // Do nothing
+    }
+
+    public void setBindArguments(String[] bindArgs) {
+        mQuery.bindAllArgsAsStrings(bindArgs);
+    }
+
+    public void cursorDeactivated() {
+        // Do nothing
+    }
+
+    public void cursorRequeried(Cursor cursor) {
+        // Do nothing
+    }
+
+    @Override
+    public String toString() {
+        return "SQLiteDirectCursorDriver: " + mSql;
+    }
+}
diff --git a/android/database/sqlite/SQLiteDiskIOException.java b/android/database/sqlite/SQLiteDiskIOException.java
new file mode 100644
index 0000000..01b2069
--- /dev/null
+++ b/android/database/sqlite/SQLiteDiskIOException.java
@@ -0,0 +1,29 @@
+/*
+ * 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 android.database.sqlite;
+
+/**
+ * An exception that indicates that an IO error occured while accessing the 
+ * SQLite database file.
+ */
+public class SQLiteDiskIOException extends SQLiteException {
+    public SQLiteDiskIOException() {}
+
+    public SQLiteDiskIOException(String error) {
+        super(error);
+    }
+}
diff --git a/android/database/sqlite/SQLiteDoneException.java b/android/database/sqlite/SQLiteDoneException.java
new file mode 100644
index 0000000..d6d3f66
--- /dev/null
+++ b/android/database/sqlite/SQLiteDoneException.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2008 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 android.database.sqlite;
+
+/**
+ * An exception that indicates that the SQLite program is done.
+ * Thrown when an operation that expects a row (such as {@link
+ * SQLiteStatement#simpleQueryForString} or {@link
+ * SQLiteStatement#simpleQueryForLong}) does not get one.
+ */
+public class SQLiteDoneException extends SQLiteException {
+    public SQLiteDoneException() {}
+
+    public SQLiteDoneException(String error) {
+        super(error);
+    }
+}
diff --git a/android/database/sqlite/SQLiteException.java b/android/database/sqlite/SQLiteException.java
new file mode 100644
index 0000000..a1d9c9f
--- /dev/null
+++ b/android/database/sqlite/SQLiteException.java
@@ -0,0 +1,35 @@
+/*
+ * 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 android.database.sqlite;
+
+import android.database.SQLException;
+
+/**
+ * A SQLite exception that indicates there was an error with SQL parsing or execution.
+ */
+public class SQLiteException extends SQLException {
+    public SQLiteException() {
+    }
+
+    public SQLiteException(String error) {
+        super(error);
+    }
+
+    public SQLiteException(String error, Throwable cause) {
+        super(error, cause);
+    }
+}
diff --git a/android/database/sqlite/SQLiteFullException.java b/android/database/sqlite/SQLiteFullException.java
new file mode 100644
index 0000000..582d930
--- /dev/null
+++ b/android/database/sqlite/SQLiteFullException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2008 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 android.database.sqlite;
+
+/**
+ * An exception that indicates that the SQLite database is full.
+ */
+public class SQLiteFullException extends SQLiteException {
+    public SQLiteFullException() {}
+
+    public SQLiteFullException(String error) {
+        super(error);
+    }
+}
diff --git a/android/database/sqlite/SQLiteGlobal.java b/android/database/sqlite/SQLiteGlobal.java
new file mode 100644
index 0000000..5e2875d
--- /dev/null
+++ b/android/database/sqlite/SQLiteGlobal.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2011 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 android.database.sqlite;
+
+import android.annotation.TestApi;
+import android.content.res.Resources;
+import android.os.StatFs;
+import android.os.SystemProperties;
+
+/**
+ * Provides access to SQLite functions that affect all database connection,
+ * such as memory management.
+ *
+ * The native code associated with SQLiteGlobal is also sets global configuration options
+ * using sqlite3_config() then calls sqlite3_initialize() to ensure that the SQLite
+ * library is properly initialized exactly once before any other framework or application
+ * code has a chance to run.
+ *
+ * Verbose SQLite logging is enabled if the "log.tag.SQLiteLog" property is set to "V".
+ * (per {@link SQLiteDebug#DEBUG_SQL_LOG}).
+ *
+ * @hide
+ */
+@TestApi
+public final class SQLiteGlobal {
+    private static final String TAG = "SQLiteGlobal";
+
+    /** @hide */
+    public static final String SYNC_MODE_FULL = "FULL";
+
+    /** @hide */
+    static final String WIPE_CHECK_FILE_SUFFIX = "-wipecheck";
+
+    private static final Object sLock = new Object();
+
+    private static int sDefaultPageSize;
+
+    private static native int nativeReleaseMemory();
+
+    /** @hide */
+    public static volatile String sDefaultSyncMode;
+
+    private SQLiteGlobal() {
+    }
+
+    /**
+     * Attempts to release memory by pruning the SQLite page cache and other
+     * internal data structures.
+     *
+     * @return The number of bytes that were freed.
+     */
+    public static int releaseMemory() {
+        return nativeReleaseMemory();
+    }
+
+    /**
+     * Gets the default page size to use when creating a database.
+     */
+    public static int getDefaultPageSize() {
+        synchronized (sLock) {
+            if (sDefaultPageSize == 0) {
+                // If there is an issue accessing /data, something is so seriously
+                // wrong that we just let the IllegalArgumentException propagate.
+                sDefaultPageSize = new StatFs("/data").getBlockSize();
+            }
+            return SystemProperties.getInt("debug.sqlite.pagesize", sDefaultPageSize);
+        }
+    }
+
+    /**
+     * Gets the default journal mode when WAL is not in use.
+     */
+    public static String getDefaultJournalMode() {
+        return SystemProperties.get("debug.sqlite.journalmode",
+                Resources.getSystem().getString(
+                com.android.internal.R.string.db_default_journal_mode));
+    }
+
+    /**
+     * Gets the journal size limit in bytes.
+     */
+    public static int getJournalSizeLimit() {
+        return SystemProperties.getInt("debug.sqlite.journalsizelimit",
+                Resources.getSystem().getInteger(
+                com.android.internal.R.integer.db_journal_size_limit));
+    }
+
+    /**
+     * Gets the default database synchronization mode when WAL is not in use.
+     */
+    public static String getDefaultSyncMode() {
+        // Use the FULL synchronous mode for system processes by default.
+        String defaultMode = sDefaultSyncMode;
+        if (defaultMode != null) {
+            return defaultMode;
+        }
+        return SystemProperties.get("debug.sqlite.syncmode",
+                Resources.getSystem().getString(
+                com.android.internal.R.string.db_default_sync_mode));
+    }
+
+    /**
+     * Gets the database synchronization mode when in WAL mode.
+     */
+    public static String getWALSyncMode() {
+        // Use the FULL synchronous mode for system processes by default.
+        String defaultMode = sDefaultSyncMode;
+        if (defaultMode != null) {
+            return defaultMode;
+        }
+        return SystemProperties.get("debug.sqlite.wal.syncmode",
+                Resources.getSystem().getString(
+                com.android.internal.R.string.db_wal_sync_mode));
+    }
+
+    /**
+     * Gets the WAL auto-checkpoint integer in database pages.
+     */
+    public static int getWALAutoCheckpoint() {
+        int value = SystemProperties.getInt("debug.sqlite.wal.autocheckpoint",
+                Resources.getSystem().getInteger(
+                com.android.internal.R.integer.db_wal_autocheckpoint));
+        return Math.max(1, value);
+    }
+
+    /**
+     * Gets the connection pool size when in WAL mode.
+     */
+    public static int getWALConnectionPoolSize() {
+        int value = SystemProperties.getInt("debug.sqlite.wal.poolsize",
+                Resources.getSystem().getInteger(
+                com.android.internal.R.integer.db_connection_pool_size));
+        return Math.max(2, value);
+    }
+
+    /**
+     * The default number of milliseconds that SQLite connection is allowed to be idle before it
+     * is closed and removed from the pool.
+     */
+    public static int getIdleConnectionTimeout() {
+        return SystemProperties.getInt("debug.sqlite.idle_connection_timeout",
+                Resources.getSystem().getInteger(
+                        com.android.internal.R.integer.db_default_idle_connection_timeout));
+    }
+
+    /**
+     * When opening a database, if the WAL file is larger than this size, we'll truncate it.
+     *
+     * (If it's 0, we do not truncate.)
+     *
+     * @hide
+     */
+    public static long getWALTruncateSize() {
+        final long setting = SQLiteCompatibilityWalFlags.getTruncateSize();
+        if (setting >= 0) {
+            return setting;
+        }
+        return SystemProperties.getInt("debug.sqlite.wal.truncatesize",
+                Resources.getSystem().getInteger(
+                        com.android.internal.R.integer.db_wal_truncate_size));
+    }
+
+    /** @hide */
+    public static boolean checkDbWipe() {
+        return false;
+    }
+}
diff --git a/android/database/sqlite/SQLiteMisuseException.java b/android/database/sqlite/SQLiteMisuseException.java
new file mode 100644
index 0000000..546ec08
--- /dev/null
+++ b/android/database/sqlite/SQLiteMisuseException.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2008 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 android.database.sqlite;
+
+/**
+ * This error can occur if the application creates a SQLiteStatement object and allows multiple
+ * threads in the application use it at the same time.
+ * Sqlite returns this error if bind and execute methods on this object occur at the same time
+ * from multiple threads, like so:
+ *     thread # 1: in execute() method of the SQLiteStatement object
+ *     while thread # 2: is in bind..() on the same object.
+ *</p>
+ * FIX this by NEVER sharing the same SQLiteStatement object between threads.
+ * Create a local instance of the SQLiteStatement whenever it is needed, use it and close it ASAP.
+ * NEVER make it globally available.
+ */
+public class SQLiteMisuseException extends SQLiteException {
+    public SQLiteMisuseException() {}
+
+    public SQLiteMisuseException(String error) {
+        super(error);
+    }
+}
diff --git a/android/database/sqlite/SQLiteOpenHelper.java b/android/database/sqlite/SQLiteOpenHelper.java
new file mode 100644
index 0000000..3341800
--- /dev/null
+++ b/android/database/sqlite/SQLiteOpenHelper.java
@@ -0,0 +1,558 @@
+/*
+ * Copyright (C) 2007 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 android.database.sqlite;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.database.DatabaseErrorHandler;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+import android.os.FileUtils;
+import android.util.Log;
+
+import java.io.File;
+import java.util.Objects;
+
+/**
+ * A helper class to manage database creation and version management.
+ *
+ * <p>You create a subclass implementing {@link #onCreate}, {@link #onUpgrade} and
+ * optionally {@link #onOpen}, and this class takes care of opening the database
+ * if it exists, creating it if it does not, and upgrading it as necessary.
+ * Transactions are used to make sure the database is always in a sensible state.
+ *
+ * <p>This class makes it easy for {@link android.content.ContentProvider}
+ * implementations to defer opening and upgrading the database until first use,
+ * to avoid blocking application startup with long-running database upgrades.
+ *
+ * <p>For an example, see the NotePadProvider class in the NotePad sample application,
+ * in the <em>samples/</em> directory of the SDK.</p>
+ *
+ * <p class="note"><strong>Note:</strong> this class assumes
+ * monotonically increasing version numbers for upgrades.</p>
+ *
+ * <p class="note"><strong>Note:</strong> the {@link AutoCloseable} interface was
+ * first added in the {@link android.os.Build.VERSION_CODES#Q} release.</p>
+ */
+public abstract class SQLiteOpenHelper implements AutoCloseable {
+    private static final String TAG = SQLiteOpenHelper.class.getSimpleName();
+
+    private final Context mContext;
+    @UnsupportedAppUsage
+    private final String mName;
+    private final int mNewVersion;
+    private final int mMinimumSupportedVersion;
+
+    private SQLiteDatabase mDatabase;
+    private boolean mIsInitializing;
+    private SQLiteDatabase.OpenParams.Builder mOpenParamsBuilder;
+
+    /**
+     * Create a helper object to create, open, and/or manage a database.
+     * This method always returns very quickly.  The database is not actually
+     * created or opened until one of {@link #getWritableDatabase} or
+     * {@link #getReadableDatabase} is called.
+     *
+     * @param context to use for locating paths to the the database
+     * @param name of the database file, or null for an in-memory database
+     * @param factory to use for creating cursor objects, or null for the default
+     * @param version number of the database (starting at 1); if the database is older,
+     *     {@link #onUpgrade} will be used to upgrade the database; if the database is
+     *     newer, {@link #onDowngrade} will be used to downgrade the database
+     */
+    public SQLiteOpenHelper(@Nullable Context context, @Nullable String name,
+            @Nullable CursorFactory factory, int version) {
+        this(context, name, factory, version, null);
+    }
+
+    /**
+     * Create a helper object to create, open, and/or manage a database.
+     * The database is not actually created or opened until one of
+     * {@link #getWritableDatabase} or {@link #getReadableDatabase} is called.
+     *
+     * <p>Accepts input param: a concrete instance of {@link DatabaseErrorHandler} to be
+     * used to handle corruption when sqlite reports database corruption.</p>
+     *
+     * @param context to use for locating paths to the the database
+     * @param name of the database file, or null for an in-memory database
+     * @param factory to use for creating cursor objects, or null for the default
+     * @param version number of the database (starting at 1); if the database is older,
+     *     {@link #onUpgrade} will be used to upgrade the database; if the database is
+     *     newer, {@link #onDowngrade} will be used to downgrade the database
+     * @param errorHandler the {@link DatabaseErrorHandler} to be used when sqlite reports database
+     * corruption, or null to use the default error handler.
+     */
+    public SQLiteOpenHelper(@Nullable Context context, @Nullable String name,
+            @Nullable CursorFactory factory, int version,
+            @Nullable DatabaseErrorHandler errorHandler) {
+        this(context, name, factory, version, 0, errorHandler);
+    }
+
+    /**
+     * Create a helper object to create, open, and/or manage a database.
+     * This method always returns very quickly.  The database is not actually
+     * created or opened until one of {@link #getWritableDatabase} or
+     * {@link #getReadableDatabase} is called.
+     *
+     * @param context to use for locating paths to the the database
+     * @param name of the database file, or null for an in-memory database
+     * @param version number of the database (starting at 1); if the database is older,
+     *     {@link #onUpgrade} will be used to upgrade the database; if the database is
+     *     newer, {@link #onDowngrade} will be used to downgrade the database
+     * @param openParams configuration parameters that are used for opening {@link SQLiteDatabase}.
+     *        Please note that {@link SQLiteDatabase#CREATE_IF_NECESSARY} flag will always be
+     *        set when the helper opens the database
+     */
+    public SQLiteOpenHelper(@Nullable Context context, @Nullable String name, int version,
+            @NonNull SQLiteDatabase.OpenParams openParams) {
+        this(context, name, version, 0, openParams.toBuilder());
+    }
+
+    /**
+     * Same as {@link #SQLiteOpenHelper(Context, String, CursorFactory, int, DatabaseErrorHandler)}
+     * but also accepts an integer minimumSupportedVersion as a convenience for upgrading very old
+     * versions of this database that are no longer supported. If a database with older version that
+     * minimumSupportedVersion is found, it is simply deleted and a new database is created with the
+     * given name and version
+     *
+     * @param context to use for locating paths to the the database
+     * @param name the name of the database file, null for a temporary in-memory database
+     * @param factory to use for creating cursor objects, null for default
+     * @param version the required version of the database
+     * @param minimumSupportedVersion the minimum version that is supported to be upgraded to
+     *            {@code version} via {@link #onUpgrade}. If the current database version is lower
+     *            than this, database is simply deleted and recreated with the version passed in
+     *            {@code version}. {@link #onBeforeDelete} is called before deleting the database
+     *            when this happens. This is 0 by default.
+     * @param errorHandler the {@link DatabaseErrorHandler} to be used when sqlite reports database
+     *            corruption, or null to use the default error handler.
+     * @see #onBeforeDelete(SQLiteDatabase)
+     * @see #SQLiteOpenHelper(Context, String, CursorFactory, int, DatabaseErrorHandler)
+     * @see #onUpgrade(SQLiteDatabase, int, int)
+     * @hide
+     */
+    public SQLiteOpenHelper(@Nullable Context context, @Nullable String name,
+            @Nullable CursorFactory factory, int version,
+            int minimumSupportedVersion, @Nullable DatabaseErrorHandler errorHandler) {
+        this(context, name, version, minimumSupportedVersion,
+                new SQLiteDatabase.OpenParams.Builder());
+        mOpenParamsBuilder.setCursorFactory(factory);
+        mOpenParamsBuilder.setErrorHandler(errorHandler);
+    }
+
+    private SQLiteOpenHelper(@Nullable Context context, @Nullable String name, int version,
+            int minimumSupportedVersion,
+            @NonNull SQLiteDatabase.OpenParams.Builder openParamsBuilder) {
+        Objects.requireNonNull(openParamsBuilder);
+        if (version < 1) throw new IllegalArgumentException("Version must be >= 1, was " + version);
+
+        mContext = context;
+        mName = name;
+        mNewVersion = version;
+        mMinimumSupportedVersion = Math.max(0, minimumSupportedVersion);
+        setOpenParamsBuilder(openParamsBuilder);
+    }
+
+    /**
+     * Return the name of the SQLite database being opened, as given to
+     * the constructor.
+     */
+    public String getDatabaseName() {
+        return mName;
+    }
+
+    /**
+     * Enables or disables the use of write-ahead logging for the database.
+     *
+     * Write-ahead logging cannot be used with read-only databases so the value of
+     * this flag is ignored if the database is opened read-only.
+     *
+     * @param enabled True if write-ahead logging should be enabled, false if it
+     * should be disabled.
+     *
+     * @see SQLiteDatabase#enableWriteAheadLogging()
+     */
+    public void setWriteAheadLoggingEnabled(boolean enabled) {
+        synchronized (this) {
+            if (mOpenParamsBuilder.isWriteAheadLoggingEnabled() != enabled) {
+                if (mDatabase != null && mDatabase.isOpen() && !mDatabase.isReadOnly()) {
+                    if (enabled) {
+                        mDatabase.enableWriteAheadLogging();
+                    } else {
+                        mDatabase.disableWriteAheadLogging();
+                    }
+                }
+                mOpenParamsBuilder.setWriteAheadLoggingEnabled(enabled);
+            }
+
+            // Compatibility WAL is disabled if an app disables or enables WAL
+            mOpenParamsBuilder.removeOpenFlags(SQLiteDatabase.ENABLE_LEGACY_COMPATIBILITY_WAL);
+        }
+    }
+
+    /**
+     * Configures <a href="https://sqlite.org/malloc.html#lookaside">lookaside memory allocator</a>
+     *
+     * <p>This method should be called from the constructor of the subclass,
+     * before opening the database, since lookaside memory configuration can only be changed
+     * when no connection is using it
+     *
+     * <p>SQLite default settings will be used, if this method isn't called.
+     * Use {@code setLookasideConfig(0,0)} to disable lookaside
+     *
+     * <p><strong>Note:</strong> Provided slotSize/slotCount configuration is just a recommendation.
+     * The system may choose different values depending on a device, e.g. lookaside allocations
+     * can be disabled on low-RAM devices
+     *
+     * @param slotSize The size in bytes of each lookaside slot.
+     * @param slotCount The total number of lookaside memory slots per database connection.
+     */
+    public void setLookasideConfig(@IntRange(from = 0) final int slotSize,
+            @IntRange(from = 0) final int slotCount) {
+        synchronized (this) {
+            if (mDatabase != null && mDatabase.isOpen()) {
+                throw new IllegalStateException(
+                        "Lookaside memory config cannot be changed after opening the database");
+            }
+            mOpenParamsBuilder.setLookasideConfig(slotSize, slotCount);
+        }
+    }
+
+    /**
+     * Sets configuration parameters that are used for opening {@link SQLiteDatabase}.
+     * <p>Please note that {@link SQLiteDatabase#CREATE_IF_NECESSARY} flag will always be set when
+     * opening the database
+     *
+     * @param openParams configuration parameters that are used for opening {@link SQLiteDatabase}.
+     * @throws IllegalStateException if the database is already open
+     */
+    public void setOpenParams(@NonNull SQLiteDatabase.OpenParams openParams) {
+        Objects.requireNonNull(openParams);
+        synchronized (this) {
+            if (mDatabase != null && mDatabase.isOpen()) {
+                throw new IllegalStateException(
+                        "OpenParams cannot be set after opening the database");
+            }
+            setOpenParamsBuilder(new SQLiteDatabase.OpenParams.Builder(openParams));
+        }
+    }
+
+    private void setOpenParamsBuilder(SQLiteDatabase.OpenParams.Builder openParamsBuilder) {
+        mOpenParamsBuilder = openParamsBuilder;
+        mOpenParamsBuilder.addOpenFlags(SQLiteDatabase.CREATE_IF_NECESSARY);
+    }
+
+    /**
+     * Sets the maximum number of milliseconds that SQLite connection is allowed to be idle
+     * before it is closed and removed from the pool.
+     *
+     * <p>This method should be called from the constructor of the subclass,
+     * before opening the database
+     *
+     * <p><b>DO NOT USE</b> this method.
+     * This feature has negative side effects that are very hard to foresee.
+     * See the javadoc of
+     * {@link SQLiteDatabase.OpenParams.Builder#setIdleConnectionTimeout(long)}
+     * for the details.
+     *
+     * @param idleConnectionTimeoutMs timeout in milliseconds. Use {@link Long#MAX_VALUE} value
+     * to allow unlimited idle connections.
+     *
+     * @see SQLiteDatabase.OpenParams.Builder#setIdleConnectionTimeout(long)
+     *
+     * @deprecated DO NOT USE this method. See the javadoc of
+     * {@link SQLiteDatabase.OpenParams.Builder#setIdleConnectionTimeout(long)}
+     * for the details.
+     */
+    @Deprecated
+    public void setIdleConnectionTimeout(@IntRange(from = 0) final long idleConnectionTimeoutMs) {
+        synchronized (this) {
+            if (mDatabase != null && mDatabase.isOpen()) {
+                throw new IllegalStateException(
+                        "Connection timeout setting cannot be changed after opening the database");
+            }
+            mOpenParamsBuilder.setIdleConnectionTimeout(idleConnectionTimeoutMs);
+        }
+    }
+
+    /**
+     * Create and/or open a database that will be used for reading and writing.
+     * The first time this is called, the database will be opened and
+     * {@link #onCreate}, {@link #onUpgrade} and/or {@link #onOpen} will be
+     * called.
+     *
+     * <p>Once opened successfully, the database is cached, so you can
+     * call this method every time you need to write to the database.
+     * (Make sure to call {@link #close} when you no longer need the database.)
+     * Errors such as bad permissions or a full disk may cause this method
+     * to fail, but future attempts may succeed if the problem is fixed.</p>
+     *
+     * <p class="caution">Database upgrade may take a long time, you
+     * should not call this method from the application main thread, including
+     * from {@link android.content.ContentProvider#onCreate ContentProvider.onCreate()}.
+     *
+     * @throws SQLiteException if the database cannot be opened for writing
+     * @return a read/write database object valid until {@link #close} is called
+     */
+    public SQLiteDatabase getWritableDatabase() {
+        synchronized (this) {
+            return getDatabaseLocked(true);
+        }
+    }
+
+    /**
+     * Create and/or open a database.  This will be the same object returned by
+     * {@link #getWritableDatabase} unless some problem, such as a full disk,
+     * requires the database to be opened read-only.  In that case, a read-only
+     * database object will be returned.  If the problem is fixed, a future call
+     * to {@link #getWritableDatabase} may succeed, in which case the read-only
+     * database object will be closed and the read/write object will be returned
+     * in the future.
+     *
+     * <p class="caution">Like {@link #getWritableDatabase}, this method may
+     * take a long time to return, so you should not call it from the
+     * application main thread, including from
+     * {@link android.content.ContentProvider#onCreate ContentProvider.onCreate()}.
+     *
+     * @throws SQLiteException if the database cannot be opened
+     * @return a database object valid until {@link #getWritableDatabase}
+     *     or {@link #close} is called.
+     */
+    public SQLiteDatabase getReadableDatabase() {
+        synchronized (this) {
+            return getDatabaseLocked(false);
+        }
+    }
+
+    private SQLiteDatabase getDatabaseLocked(boolean writable) {
+        if (mDatabase != null) {
+            if (!mDatabase.isOpen()) {
+                // Darn!  The user closed the database by calling mDatabase.close().
+                mDatabase = null;
+            } else if (!writable || !mDatabase.isReadOnly()) {
+                // The database is already open for business.
+                return mDatabase;
+            }
+        }
+
+        if (mIsInitializing) {
+            throw new IllegalStateException("getDatabase called recursively");
+        }
+
+        SQLiteDatabase db = mDatabase;
+        try {
+            mIsInitializing = true;
+
+            if (db != null) {
+                if (writable && db.isReadOnly()) {
+                    db.reopenReadWrite();
+                }
+            } else if (mName == null) {
+                db = SQLiteDatabase.createInMemory(mOpenParamsBuilder.build());
+            } else {
+                final File filePath = mContext.getDatabasePath(mName);
+                SQLiteDatabase.OpenParams params = mOpenParamsBuilder.build();
+                try {
+                    db = SQLiteDatabase.openDatabase(filePath, params);
+                    // Keep pre-O-MR1 behavior by resetting file permissions to 660
+                    setFilePermissionsForDb(filePath.getPath());
+                } catch (SQLException ex) {
+                    if (writable) {
+                        throw ex;
+                    }
+                    Log.e(TAG, "Couldn't open " + mName
+                            + " for writing (will try read-only):", ex);
+                    params = params.toBuilder().addOpenFlags(SQLiteDatabase.OPEN_READONLY).build();
+                    db = SQLiteDatabase.openDatabase(filePath, params);
+                }
+            }
+
+            onConfigure(db);
+
+            final int version = db.getVersion();
+            if (version != mNewVersion) {
+                if (db.isReadOnly()) {
+                    throw new SQLiteException("Can't upgrade read-only database from version " +
+                            db.getVersion() + " to " + mNewVersion + ": " + mName);
+                }
+
+                if (version > 0 && version < mMinimumSupportedVersion) {
+                    File databaseFile = new File(db.getPath());
+                    onBeforeDelete(db);
+                    db.close();
+                    if (SQLiteDatabase.deleteDatabase(databaseFile)) {
+                        mIsInitializing = false;
+                        return getDatabaseLocked(writable);
+                    } else {
+                        throw new IllegalStateException("Unable to delete obsolete database "
+                                + mName + " with version " + version);
+                    }
+                } else {
+                    db.beginTransaction();
+                    try {
+                        if (version == 0) {
+                            onCreate(db);
+                        } else {
+                            if (version > mNewVersion) {
+                                onDowngrade(db, version, mNewVersion);
+                            } else {
+                                onUpgrade(db, version, mNewVersion);
+                            }
+                        }
+                        db.setVersion(mNewVersion);
+                        db.setTransactionSuccessful();
+                    } finally {
+                        db.endTransaction();
+                    }
+                }
+            }
+
+            onOpen(db);
+
+            if (db.isReadOnly()) {
+                Log.w(TAG, "Opened " + mName + " in read-only mode");
+            }
+
+            mDatabase = db;
+            return db;
+        } finally {
+            mIsInitializing = false;
+            if (db != null && db != mDatabase) {
+                db.close();
+            }
+        }
+    }
+
+    private static void setFilePermissionsForDb(String dbPath) {
+        int perms = FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP | FileUtils.S_IWGRP;
+        FileUtils.setPermissions(dbPath, perms, -1, -1);
+    }
+
+    /**
+     * Close any open database object.
+     */
+    public synchronized void close() {
+        if (mIsInitializing) throw new IllegalStateException("Closed during initialization");
+
+        if (mDatabase != null && mDatabase.isOpen()) {
+            mDatabase.close();
+            mDatabase = null;
+        }
+    }
+
+    /**
+     * Called when the database connection is being configured, to enable features such as
+     * write-ahead logging or foreign key support.
+     * <p>
+     * This method is called before {@link #onCreate}, {@link #onUpgrade}, {@link #onDowngrade}, or
+     * {@link #onOpen} are called. It should not modify the database except to configure the
+     * database connection as required.
+     * </p>
+     * <p>
+     * This method should only call methods that configure the parameters of the database
+     * connection, such as {@link SQLiteDatabase#enableWriteAheadLogging}
+     * {@link SQLiteDatabase#setForeignKeyConstraintsEnabled}, {@link SQLiteDatabase#setLocale},
+     * {@link SQLiteDatabase#setMaximumSize}, or executing PRAGMA statements.
+     * </p>
+     *
+     * @param db The database.
+     */
+    public void onConfigure(SQLiteDatabase db) {}
+
+    /**
+     * Called before the database is deleted when the version returned by
+     * {@link SQLiteDatabase#getVersion()} is lower than the minimum supported version passed (if at
+     * all) while creating this helper. After the database is deleted, a fresh database with the
+     * given version is created. This will be followed by {@link #onConfigure(SQLiteDatabase)} and
+     * {@link #onCreate(SQLiteDatabase)} being called with a new SQLiteDatabase object
+     *
+     * @param db the database opened with this helper
+     * @see #SQLiteOpenHelper(Context, String, CursorFactory, int, int, DatabaseErrorHandler)
+     * @hide
+     */
+    public void onBeforeDelete(SQLiteDatabase db) {
+    }
+
+    /**
+     * Called when the database is created for the first time. This is where the
+     * creation of tables and the initial population of the tables should happen.
+     *
+     * @param db The database.
+     */
+    public abstract void onCreate(SQLiteDatabase db);
+
+    /**
+     * Called when the database needs to be upgraded. The implementation
+     * should use this method to drop tables, add tables, or do anything else it
+     * needs to upgrade to the new schema version.
+     *
+     * <p>
+     * The SQLite ALTER TABLE documentation can be found
+     * <a href="http://sqlite.org/lang_altertable.html">here</a>. If you add new columns
+     * you can use ALTER TABLE to insert them into a live table. If you rename or remove columns
+     * you can use ALTER TABLE to rename the old table, then create the new table and then
+     * populate the new table with the contents of the old table.
+     * </p><p>
+     * This method executes within a transaction.  If an exception is thrown, all changes
+     * will automatically be rolled back.
+     * </p>
+     *
+     * @param db The database.
+     * @param oldVersion The old database version.
+     * @param newVersion The new database version.
+     */
+    public abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
+
+    /**
+     * Called when the database needs to be downgraded. This is strictly similar to
+     * {@link #onUpgrade} method, but is called whenever current version is newer than requested one.
+     * However, this method is not abstract, so it is not mandatory for a customer to
+     * implement it. If not overridden, default implementation will reject downgrade and
+     * throws SQLiteException
+     *
+     * <p>
+     * This method executes within a transaction.  If an exception is thrown, all changes
+     * will automatically be rolled back.
+     * </p>
+     *
+     * @param db The database.
+     * @param oldVersion The old database version.
+     * @param newVersion The new database version.
+     */
+    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        throw new SQLiteException("Can't downgrade database from version " +
+                oldVersion + " to " + newVersion);
+    }
+
+    /**
+     * Called when the database has been opened.  The implementation
+     * should check {@link SQLiteDatabase#isReadOnly} before updating the
+     * database.
+     * <p>
+     * This method is called after the database connection has been configured
+     * and after the database schema has been created, upgraded or downgraded as necessary.
+     * If the database connection must be configured in some way before the schema
+     * is created, upgraded, or downgraded, do it in {@link #onConfigure} instead.
+     * </p>
+     *
+     * @param db The database.
+     */
+    public void onOpen(SQLiteDatabase db) {}
+}
diff --git a/android/database/sqlite/SQLiteOutOfMemoryException.java b/android/database/sqlite/SQLiteOutOfMemoryException.java
new file mode 100644
index 0000000..98ce8b5
--- /dev/null
+++ b/android/database/sqlite/SQLiteOutOfMemoryException.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2010 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 android.database.sqlite;
+
+public class SQLiteOutOfMemoryException extends SQLiteException {
+    public SQLiteOutOfMemoryException() {}
+
+    public SQLiteOutOfMemoryException(String error) {
+        super(error);
+    }
+}
diff --git a/android/database/sqlite/SQLiteProgram.java b/android/database/sqlite/SQLiteProgram.java
new file mode 100644
index 0000000..de1c543
--- /dev/null
+++ b/android/database/sqlite/SQLiteProgram.java
@@ -0,0 +1,221 @@
+/*
+ * 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 android.database.sqlite;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.database.DatabaseUtils;
+import android.os.CancellationSignal;
+
+import java.util.Arrays;
+
+/**
+ * A base class for compiled SQLite programs.
+ * <p>
+ * This class is not thread-safe.
+ * </p>
+ */
+public abstract class SQLiteProgram extends SQLiteClosable {
+    private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+    private final SQLiteDatabase mDatabase;
+    @UnsupportedAppUsage
+    private final String mSql;
+    private final boolean mReadOnly;
+    private final String[] mColumnNames;
+    private final int mNumParameters;
+    @UnsupportedAppUsage
+    private final Object[] mBindArgs;
+
+    SQLiteProgram(SQLiteDatabase db, String sql, Object[] bindArgs,
+            CancellationSignal cancellationSignalForPrepare) {
+        mDatabase = db;
+        mSql = sql.trim();
+
+        int n = DatabaseUtils.getSqlStatementType(mSql);
+        switch (n) {
+            case DatabaseUtils.STATEMENT_BEGIN:
+            case DatabaseUtils.STATEMENT_COMMIT:
+            case DatabaseUtils.STATEMENT_ABORT:
+                mReadOnly = false;
+                mColumnNames = EMPTY_STRING_ARRAY;
+                mNumParameters = 0;
+                break;
+
+            default:
+                boolean assumeReadOnly = (n == DatabaseUtils.STATEMENT_SELECT);
+                SQLiteStatementInfo info = new SQLiteStatementInfo();
+                db.getThreadSession().prepare(mSql,
+                        db.getThreadDefaultConnectionFlags(assumeReadOnly),
+                        cancellationSignalForPrepare, info);
+                mReadOnly = info.readOnly;
+                mColumnNames = info.columnNames;
+                mNumParameters = info.numParameters;
+                break;
+        }
+
+        if (bindArgs != null && bindArgs.length > mNumParameters) {
+            throw new IllegalArgumentException("Too many bind arguments.  "
+                    + bindArgs.length + " arguments were provided but the statement needs "
+                    + mNumParameters + " arguments.");
+        }
+
+        if (mNumParameters != 0) {
+            mBindArgs = new Object[mNumParameters];
+            if (bindArgs != null) {
+                System.arraycopy(bindArgs, 0, mBindArgs, 0, bindArgs.length);
+            }
+        } else {
+            mBindArgs = null;
+        }
+    }
+
+    final SQLiteDatabase getDatabase() {
+        return mDatabase;
+    }
+
+    final String getSql() {
+        return mSql;
+    }
+
+    final Object[] getBindArgs() {
+        return mBindArgs;
+    }
+
+    final String[] getColumnNames() {
+        return mColumnNames;
+    }
+
+    /** @hide */
+    protected final SQLiteSession getSession() {
+        return mDatabase.getThreadSession();
+    }
+
+    /** @hide */
+    protected final int getConnectionFlags() {
+        return mDatabase.getThreadDefaultConnectionFlags(mReadOnly);
+    }
+
+    /** @hide */
+    protected final void onCorruption() {
+        mDatabase.onCorruption();
+    }
+
+    /**
+     * Unimplemented.
+     * @deprecated This method is deprecated and must not be used.
+     */
+    @Deprecated
+    public final int getUniqueId() {
+        return -1;
+    }
+
+    /**
+     * Bind a NULL value to this statement. The value remains bound until
+     * {@link #clearBindings} is called.
+     *
+     * @param index The 1-based index to the parameter to bind null to
+     */
+    public void bindNull(int index) {
+        bind(index, null);
+    }
+
+    /**
+     * Bind a long value to this statement. The value remains bound until
+     * {@link #clearBindings} is called.
+     *addToBindArgs
+     * @param index The 1-based index to the parameter to bind
+     * @param value The value to bind
+     */
+    public void bindLong(int index, long value) {
+        bind(index, value);
+    }
+
+    /**
+     * Bind a double value to this statement. The value remains bound until
+     * {@link #clearBindings} is called.
+     *
+     * @param index The 1-based index to the parameter to bind
+     * @param value The value to bind
+     */
+    public void bindDouble(int index, double value) {
+        bind(index, value);
+    }
+
+    /**
+     * Bind a String value to this statement. The value remains bound until
+     * {@link #clearBindings} is called.
+     *
+     * @param index The 1-based index to the parameter to bind
+     * @param value The value to bind, must not be null
+     */
+    public void bindString(int index, String value) {
+        if (value == null) {
+            throw new IllegalArgumentException("the bind value at index " + index + " is null");
+        }
+        bind(index, value);
+    }
+
+    /**
+     * Bind a byte array value to this statement. The value remains bound until
+     * {@link #clearBindings} is called.
+     *
+     * @param index The 1-based index to the parameter to bind
+     * @param value The value to bind, must not be null
+     */
+    public void bindBlob(int index, byte[] value) {
+        if (value == null) {
+            throw new IllegalArgumentException("the bind value at index " + index + " is null");
+        }
+        bind(index, value);
+    }
+
+    /**
+     * Clears all existing bindings. Unset bindings are treated as NULL.
+     */
+    public void clearBindings() {
+        if (mBindArgs != null) {
+            Arrays.fill(mBindArgs, null);
+        }
+    }
+
+    /**
+     * Given an array of String bindArgs, this method binds all of them in one single call.
+     *
+     * @param bindArgs the String array of bind args, none of which must be null.
+     */
+    public void bindAllArgsAsStrings(String[] bindArgs) {
+        if (bindArgs != null) {
+            for (int i = bindArgs.length; i != 0; i--) {
+                bindString(i, bindArgs[i - 1]);
+            }
+        }
+    }
+
+    @Override
+    protected void onAllReferencesReleased() {
+        clearBindings();
+    }
+
+    private void bind(int index, Object value) {
+        if (index < 1 || index > mNumParameters) {
+            throw new IllegalArgumentException("Cannot bind argument at index "
+                    + index + " because the index is out of range.  "
+                    + "The statement has " + mNumParameters + " parameters.");
+        }
+        mBindArgs[index - 1] = value;
+    }
+}
diff --git a/android/database/sqlite/SQLiteQuery.java b/android/database/sqlite/SQLiteQuery.java
new file mode 100644
index 0000000..62bcc20
--- /dev/null
+++ b/android/database/sqlite/SQLiteQuery.java
@@ -0,0 +1,84 @@
+/*
+ * 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 android.database.sqlite;
+
+import android.database.CursorWindow;
+import android.os.CancellationSignal;
+import android.os.OperationCanceledException;
+import android.util.Log;
+
+/**
+ * Represents a query that reads the resulting rows into a {@link SQLiteQuery}.
+ * This class is used by {@link SQLiteCursor} and isn't useful itself.
+ * <p>
+ * This class is not thread-safe.
+ * </p>
+ */
+public final class SQLiteQuery extends SQLiteProgram {
+    private static final String TAG = "SQLiteQuery";
+
+    private final CancellationSignal mCancellationSignal;
+
+    SQLiteQuery(SQLiteDatabase db, String query, CancellationSignal cancellationSignal) {
+        super(db, query, null, cancellationSignal);
+
+        mCancellationSignal = cancellationSignal;
+    }
+
+    /**
+     * Reads rows into a buffer.
+     *
+     * @param window The window to fill into
+     * @param startPos The start position for filling the window.
+     * @param requiredPos The position of a row that MUST be in the window.
+     * If it won't fit, then the query should discard part of what it filled.
+     * @param countAllRows True to count all rows that the query would
+     * return regardless of whether they fit in the window.
+     * @return Number of rows that were enumerated.  Might not be all rows
+     * unless countAllRows is true.
+     *
+     * @throws SQLiteException if an error occurs.
+     * @throws OperationCanceledException if the operation was canceled.
+     */
+    int fillWindow(CursorWindow window, int startPos, int requiredPos, boolean countAllRows) {
+        acquireReference();
+        try {
+            window.acquireReference();
+            try {
+                int numRows = getSession().executeForCursorWindow(getSql(), getBindArgs(),
+                        window, startPos, requiredPos, countAllRows, getConnectionFlags(),
+                        mCancellationSignal);
+                return numRows;
+            } catch (SQLiteDatabaseCorruptException ex) {
+                onCorruption();
+                throw ex;
+            } catch (SQLiteException ex) {
+                Log.e(TAG, "exception: " + ex.getMessage() + "; query: " + getSql());
+                throw ex;
+            } finally {
+                window.releaseReference();
+            }
+        } finally {
+            releaseReference();
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "SQLiteQuery: " + getSql();
+    }
+}
diff --git a/android/database/sqlite/SQLiteQueryBuilder.java b/android/database/sqlite/SQLiteQueryBuilder.java
new file mode 100644
index 0000000..36ec67e
--- /dev/null
+++ b/android/database/sqlite/SQLiteQueryBuilder.java
@@ -0,0 +1,1203 @@
+/*
+ * 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 android.database.sqlite;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.os.Build;
+import android.os.CancellationSignal;
+import android.os.OperationCanceledException;
+import android.provider.BaseColumns;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.util.ArrayUtils;
+
+import libcore.util.EmptyArray;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This is a convenience class that helps build SQL queries to be sent to
+ * {@link SQLiteDatabase} objects.
+ */
+public class SQLiteQueryBuilder {
+    private static final String TAG = "SQLiteQueryBuilder";
+
+    private static final Pattern sAggregationPattern = Pattern.compile(
+            "(?i)(AVG|COUNT|MAX|MIN|SUM|TOTAL|GROUP_CONCAT)\\((.+)\\)");
+
+    private Map<String, String> mProjectionMap = null;
+    private Collection<Pattern> mProjectionGreylist = null;
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    private String mTables = "";
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    private StringBuilder mWhereClause = null;  // lazily created
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    private boolean mDistinct;
+    private SQLiteDatabase.CursorFactory mFactory;
+
+    private static final int STRICT_PARENTHESES = 1 << 0;
+    private static final int STRICT_COLUMNS = 1 << 1;
+    private static final int STRICT_GRAMMAR = 1 << 2;
+
+    private int mStrictFlags;
+
+    public SQLiteQueryBuilder() {
+        mDistinct = false;
+        mFactory = null;
+    }
+
+    /**
+     * Mark the query as {@code DISTINCT}.
+     *
+     * @param distinct if true the query is {@code DISTINCT}, otherwise it isn't
+     */
+    public void setDistinct(boolean distinct) {
+        mDistinct = distinct;
+    }
+
+    /**
+     * Get if the query is marked as {@code DISTINCT}, as last configured by
+     * {@link #setDistinct(boolean)}.
+     */
+    public boolean isDistinct() {
+        return mDistinct;
+    }
+
+    /**
+     * Returns the list of tables being queried
+     *
+     * @return the list of tables being queried
+     */
+    public @Nullable String getTables() {
+        return mTables;
+    }
+
+    /**
+     * Sets the list of tables to query. Multiple tables can be specified to perform a join.
+     * For example:
+     *   setTables("foo, bar")
+     *   setTables("foo LEFT OUTER JOIN bar ON (foo.id = bar.foo_id)")
+     *
+     * @param inTables the list of tables to query on
+     */
+    public void setTables(@Nullable String inTables) {
+        mTables = inTables;
+    }
+
+    /**
+     * Append a chunk to the {@code WHERE} clause of the query. All chunks appended are surrounded
+     * by parenthesis and {@code AND}ed with the selection passed to {@link #query}. The final
+     * {@code WHERE} clause looks like:
+     * <p>
+     * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
+     *
+     * @param inWhere the chunk of text to append to the {@code WHERE} clause.
+     */
+    public void appendWhere(@NonNull CharSequence inWhere) {
+        if (mWhereClause == null) {
+            mWhereClause = new StringBuilder(inWhere.length() + 16);
+        }
+        mWhereClause.append(inWhere);
+    }
+
+    /**
+     * Append a chunk to the {@code WHERE} clause of the query. All chunks appended are surrounded
+     * by parenthesis and ANDed with the selection passed to {@link #query}. The final
+     * {@code WHERE} clause looks like:
+     * <p>
+     * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
+     *
+     * @param inWhere the chunk of text to append to the {@code WHERE} clause. it will be escaped
+     * to avoid SQL injection attacks
+     */
+    public void appendWhereEscapeString(@NonNull String inWhere) {
+        if (mWhereClause == null) {
+            mWhereClause = new StringBuilder(inWhere.length() + 16);
+        }
+        DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere);
+    }
+
+    /**
+     * Add a standalone chunk to the {@code WHERE} clause of this query.
+     * <p>
+     * This method differs from {@link #appendWhere(CharSequence)} in that it
+     * automatically appends {@code AND} to any existing {@code WHERE} clause
+     * already under construction before appending the given standalone
+     * expression wrapped in parentheses.
+     *
+     * @param inWhere the standalone expression to append to the {@code WHERE}
+     *            clause. It will be wrapped in parentheses when it's appended.
+     */
+    public void appendWhereStandalone(@NonNull CharSequence inWhere) {
+        if (mWhereClause == null) {
+            mWhereClause = new StringBuilder(inWhere.length() + 16);
+        }
+        if (mWhereClause.length() > 0) {
+            mWhereClause.append(" AND ");
+        }
+        mWhereClause.append('(').append(inWhere).append(')');
+    }
+
+    /**
+     * Sets the projection map for the query.  The projection map maps
+     * from column names that the caller passes into query to database
+     * column names. This is useful for renaming columns as well as
+     * disambiguating column names when doing joins. For example you
+     * could map "name" to "people.name".  If a projection map is set
+     * it must contain all column names the user may request, even if
+     * the key and value are the same.
+     *
+     * @param columnMap maps from the user column names to the database column names
+     */
+    public void setProjectionMap(@Nullable Map<String, String> columnMap) {
+        mProjectionMap = columnMap;
+    }
+
+    /**
+     * Gets the projection map for the query, as last configured by
+     * {@link #setProjectionMap(Map)}.
+     */
+    public @Nullable Map<String, String> getProjectionMap() {
+        return mProjectionMap;
+    }
+
+    /**
+     * Sets a projection greylist of columns that will be allowed through, even
+     * when {@link #setStrict(boolean)} is enabled. This provides a way for
+     * abusive custom columns like {@code COUNT(*)} to continue working.
+     */
+    public void setProjectionGreylist(@Nullable Collection<Pattern> projectionGreylist) {
+        mProjectionGreylist = projectionGreylist;
+    }
+
+    /**
+     * Gets the projection greylist for the query, as last configured by
+     * {@link #setProjectionGreylist}.
+     */
+    public @Nullable Collection<Pattern> getProjectionGreylist() {
+        return mProjectionGreylist;
+    }
+
+    /** {@hide} */
+    @Deprecated
+    public void setProjectionAggregationAllowed(boolean projectionAggregationAllowed) {
+    }
+
+    /** {@hide} */
+    @Deprecated
+    public boolean isProjectionAggregationAllowed() {
+        return true;
+    }
+
+    /**
+     * Sets the cursor factory to be used for the query.  You can use
+     * one factory for all queries on a database but it is normally
+     * easier to specify the factory when doing this query.
+     *
+     * @param factory the factory to use.
+     */
+    public void setCursorFactory(@Nullable SQLiteDatabase.CursorFactory factory) {
+        mFactory = factory;
+    }
+
+    /**
+     * Gets the cursor factory to be used for the query, as last configured by
+     * {@link #setCursorFactory(android.database.sqlite.SQLiteDatabase.CursorFactory)}.
+     */
+    public @Nullable SQLiteDatabase.CursorFactory getCursorFactory() {
+        return mFactory;
+    }
+
+    /**
+     * When set, the selection is verified against malicious arguments. When
+     * using this class to create a statement using
+     * {@link #buildQueryString(boolean, String, String[], String, String, String, String, String)},
+     * non-numeric limits will raise an exception. If a projection map is
+     * specified, fields not in that map will be ignored. If this class is used
+     * to execute the statement directly using
+     * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String)}
+     * or
+     * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String, String)},
+     * additionally also parenthesis escaping selection are caught. To
+     * summarize: To get maximum protection against malicious third party apps
+     * (for example content provider consumers), make sure to do the following:
+     * <ul>
+     * <li>Set this value to true</li>
+     * <li>Use a projection map</li>
+     * <li>Use one of the query overloads instead of getting the statement as a
+     * sql string</li>
+     * </ul>
+     * <p>
+     * This feature is disabled by default on each newly constructed
+     * {@link SQLiteQueryBuilder} and needs to be manually enabled.
+     */
+    public void setStrict(boolean strict) {
+        if (strict) {
+            mStrictFlags |= STRICT_PARENTHESES;
+        } else {
+            mStrictFlags &= ~STRICT_PARENTHESES;
+        }
+    }
+
+    /**
+     * Get if the query is marked as strict, as last configured by
+     * {@link #setStrict(boolean)}.
+     */
+    public boolean isStrict() {
+        return (mStrictFlags & STRICT_PARENTHESES) != 0;
+    }
+
+    /**
+     * When enabled, verify that all projections and {@link ContentValues} only
+     * contain valid columns as defined by {@link #setProjectionMap(Map)}.
+     * <p>
+     * This enforcement applies to {@link #insert}, {@link #query}, and
+     * {@link #update} operations. Any enforcement failures will throw an
+     * {@link IllegalArgumentException}.
+     * <p>
+     * This feature is disabled by default on each newly constructed
+     * {@link SQLiteQueryBuilder} and needs to be manually enabled.
+     */
+    public void setStrictColumns(boolean strictColumns) {
+        if (strictColumns) {
+            mStrictFlags |= STRICT_COLUMNS;
+        } else {
+            mStrictFlags &= ~STRICT_COLUMNS;
+        }
+    }
+
+    /**
+     * Get if the query is marked as strict, as last configured by
+     * {@link #setStrictColumns(boolean)}.
+     */
+    public boolean isStrictColumns() {
+        return (mStrictFlags & STRICT_COLUMNS) != 0;
+    }
+
+    /**
+     * When enabled, verify that all untrusted SQL conforms to a restricted SQL
+     * grammar. Here are the restrictions applied:
+     * <ul>
+     * <li>In {@code WHERE} and {@code HAVING} clauses: subqueries, raising, and
+     * windowing terms are rejected.
+     * <li>In {@code GROUP BY} clauses: only valid columns are allowed.
+     * <li>In {@code ORDER BY} clauses: only valid columns, collation, and
+     * ordering terms are allowed.
+     * <li>In {@code LIMIT} clauses: only numerical values and offset terms are
+     * allowed.
+     * </ul>
+     * All column references must be valid as defined by
+     * {@link #setProjectionMap(Map)}.
+     * <p>
+     * This enforcement applies to {@link #query}, {@link #update} and
+     * {@link #delete} operations. This enforcement does not apply to trusted
+     * inputs, such as those provided by {@link #appendWhere}. Any enforcement
+     * failures will throw an {@link IllegalArgumentException}.
+     * <p>
+     * This feature is disabled by default on each newly constructed
+     * {@link SQLiteQueryBuilder} and needs to be manually enabled.
+     */
+    public void setStrictGrammar(boolean strictGrammar) {
+        if (strictGrammar) {
+            mStrictFlags |= STRICT_GRAMMAR;
+        } else {
+            mStrictFlags &= ~STRICT_GRAMMAR;
+        }
+    }
+
+    /**
+     * Get if the query is marked as strict, as last configured by
+     * {@link #setStrictGrammar(boolean)}.
+     */
+    public boolean isStrictGrammar() {
+        return (mStrictFlags & STRICT_GRAMMAR) != 0;
+    }
+
+    /**
+     * Build an SQL query string from the given clauses.
+     *
+     * @param distinct true if you want each row to be unique, false otherwise.
+     * @param tables The table names 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 where A filter declaring which rows to return, formatted as an SQL
+     *            {@code WHERE} clause (excluding the {@code WHERE} itself). Passing {@code null} will
+     *            return all rows for the given URL.
+     * @param groupBy A filter declaring how to group rows, formatted as an SQL
+     *            {@code GROUP BY} clause (excluding the {@code GROUP BY} itself). Passing {@code 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 {@code HAVING}
+     *            clause (excluding the {@code 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 {@code ORDER BY} clause
+     *            (excluding the {@code 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 {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
+     * @return the SQL query string
+     */
+    public static String buildQueryString(
+            boolean distinct, String tables, String[] columns, String where,
+            String groupBy, String having, String orderBy, String limit) {
+        if (TextUtils.isEmpty(groupBy) && !TextUtils.isEmpty(having)) {
+            throw new IllegalArgumentException(
+                    "HAVING clauses are only permitted when using a groupBy clause");
+        }
+
+        StringBuilder query = new StringBuilder(120);
+
+        query.append("SELECT ");
+        if (distinct) {
+            query.append("DISTINCT ");
+        }
+        if (columns != null && columns.length != 0) {
+            appendColumns(query, columns);
+        } else {
+            query.append("* ");
+        }
+        query.append("FROM ");
+        query.append(tables);
+        appendClause(query, " WHERE ", where);
+        appendClause(query, " GROUP BY ", groupBy);
+        appendClause(query, " HAVING ", having);
+        appendClause(query, " ORDER BY ", orderBy);
+        appendClause(query, " LIMIT ", limit);
+
+        return query.toString();
+    }
+
+    private static void appendClause(StringBuilder s, String name, String clause) {
+        if (!TextUtils.isEmpty(clause)) {
+            s.append(name);
+            s.append(clause);
+        }
+    }
+
+    /**
+     * Add the names that are non-null in columns to s, separating
+     * them with commas.
+     */
+    public static void appendColumns(StringBuilder s, String[] columns) {
+        int n = columns.length;
+
+        for (int i = 0; i < n; i++) {
+            String column = columns[i];
+
+            if (column != null) {
+                if (i > 0) {
+                    s.append(", ");
+                }
+                s.append(column);
+            }
+        }
+        s.append(' ');
+    }
+
+    /**
+     * Perform a query by combining all current settings and the
+     * information passed into this method.
+     *
+     * @param db the database to query on
+     * @param projectionIn 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 {@code WHERE} clause (excluding the {@code WHERE}
+     *   itself). Passing null will return all rows for the given URL.
+     * @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 {@code GROUP BY} clause (excluding the {@code 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 {@code HAVING} clause (excluding the {@code HAVING} itself).  Passing
+     *   null will cause all row groups to be included, and is
+     *   required when row grouping is not being used.
+     * @param sortOrder How to order the rows, formatted as an SQL
+     *   {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
+     *   will use the default sort order, which may be unordered.
+     * @return a cursor over the result set
+     * @see android.content.ContentResolver#query(android.net.Uri, String[],
+     *      String, String[], String)
+     */
+    public Cursor query(SQLiteDatabase db, String[] projectionIn,
+            String selection, String[] selectionArgs, String groupBy,
+            String having, String sortOrder) {
+        return query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder,
+                null /* limit */, null /* cancellationSignal */);
+    }
+
+    /**
+     * Perform a query by combining all current settings and the
+     * information passed into this method.
+     *
+     * @param db the database to query on
+     * @param projectionIn 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 {@code WHERE} clause (excluding the {@code WHERE}
+     *   itself). Passing null will return all rows for the given URL.
+     * @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 {@code GROUP BY} clause (excluding the {@code 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 {@code HAVING} clause (excluding the {@code HAVING} itself).  Passing
+     *   null will cause all row groups to be included, and is
+     *   required when row grouping is not being used.
+     * @param sortOrder How to order the rows, formatted as an SQL
+     *   {@code ORDER BY} clause (excluding the {@code 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 {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
+     * @return a cursor over the result set
+     * @see android.content.ContentResolver#query(android.net.Uri, String[],
+     *      String, String[], String)
+     */
+    public Cursor query(SQLiteDatabase db, String[] projectionIn,
+            String selection, String[] selectionArgs, String groupBy,
+            String having, String sortOrder, String limit) {
+        return query(db, projectionIn, selection, selectionArgs,
+                groupBy, having, sortOrder, limit, null);
+    }
+
+    /**
+     * Perform a query by combining all current settings and the
+     * information passed into this method.
+     *
+     * @param db the database to query on
+     * @param projectionIn 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 {@code WHERE} clause (excluding the {@code WHERE}
+     *   itself). Passing null will return all rows for the given URL.
+     * @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 {@code GROUP BY} clause (excluding the {@code 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 {@code HAVING} clause (excluding the {@code HAVING} itself).  Passing
+     *   null will cause all row groups to be included, and is
+     *   required when row grouping is not being used.
+     * @param sortOrder How to order the rows, formatted as an SQL
+     *   {@code ORDER BY} clause (excluding the {@code 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 {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * If the operation is canceled, then {@link OperationCanceledException} will be thrown
+     * when the query is executed.
+     * @return a cursor over the result set
+     * @see android.content.ContentResolver#query(android.net.Uri, String[],
+     *      String, String[], String)
+     */
+    public Cursor query(SQLiteDatabase db, String[] projectionIn,
+            String selection, String[] selectionArgs, String groupBy,
+            String having, String sortOrder, String limit, CancellationSignal cancellationSignal) {
+        if (mTables == null) {
+            return null;
+        }
+
+        final String sql;
+        final String unwrappedSql = buildQuery(
+                projectionIn, selection, groupBy, having,
+                sortOrder, limit);
+
+        if (isStrictColumns()) {
+            enforceStrictColumns(projectionIn);
+        }
+        if (isStrictGrammar()) {
+            enforceStrictGrammar(selection, groupBy, having, sortOrder, limit);
+        }
+        if (isStrict()) {
+            // Validate the user-supplied selection to detect syntactic anomalies
+            // in the selection string that could indicate a SQL injection attempt.
+            // The idea is to ensure that the selection clause is a valid SQL expression
+            // by compiling it twice: once wrapped in parentheses and once as
+            // originally specified. An attacker cannot create an expression that
+            // would escape the SQL expression while maintaining balanced parentheses
+            // in both the wrapped and original forms.
+
+            // NOTE: The ordering of the below operations is important; we must
+            // execute the wrapped query to ensure the untrusted clause has been
+            // fully isolated.
+
+            // Validate the unwrapped query
+            db.validateSql(unwrappedSql, cancellationSignal); // will throw if query is invalid
+
+            // Execute wrapped query for extra protection
+            final String wrappedSql = buildQuery(projectionIn, wrap(selection), groupBy,
+                    wrap(having), sortOrder, limit);
+            sql = wrappedSql;
+        } else {
+            // Execute unwrapped query
+            sql = unwrappedSql;
+        }
+
+        final String[] sqlArgs = selectionArgs;
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            if (Build.IS_DEBUGGABLE) {
+                Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
+            } else {
+                Log.d(TAG, sql);
+            }
+        }
+        return db.rawQueryWithFactory(
+                mFactory, sql, sqlArgs,
+                SQLiteDatabase.findEditTable(mTables),
+                cancellationSignal); // will throw if query is invalid
+    }
+
+    /**
+     * Perform an insert by combining all current settings and the
+     * information passed into this method.
+     *
+     * @param db the database to insert on
+     * @return the row ID of the newly inserted row, or -1 if an error occurred
+     */
+    public long insert(@NonNull SQLiteDatabase db, @NonNull ContentValues values) {
+        Objects.requireNonNull(mTables, "No tables defined");
+        Objects.requireNonNull(db, "No database defined");
+        Objects.requireNonNull(values, "No values defined");
+
+        if (isStrictColumns()) {
+            enforceStrictColumns(values);
+        }
+
+        final String sql = buildInsert(values);
+
+        final ArrayMap<String, Object> rawValues = values.getValues();
+        final int valuesLength = rawValues.size();
+        final Object[] sqlArgs = new Object[valuesLength];
+        for (int i = 0; i < sqlArgs.length; i++) {
+            sqlArgs[i] = rawValues.valueAt(i);
+        }
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            if (Build.IS_DEBUGGABLE) {
+                Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
+            } else {
+                Log.d(TAG, sql);
+            }
+        }
+        return db.executeSql(sql, sqlArgs);
+    }
+
+    /**
+     * Perform an update by combining all current settings and the
+     * information passed into this method.
+     *
+     * @param db the database to update on
+     * @param selection A filter declaring which rows to return,
+     *   formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
+     *   itself). Passing null will return all rows for the given URL.
+     * @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.
+     * @return the number of rows updated
+     */
+    public int update(@NonNull SQLiteDatabase db, @NonNull ContentValues values,
+            @Nullable String selection, @Nullable String[] selectionArgs) {
+        Objects.requireNonNull(mTables, "No tables defined");
+        Objects.requireNonNull(db, "No database defined");
+        Objects.requireNonNull(values, "No values defined");
+
+        final String sql;
+        final String unwrappedSql = buildUpdate(values, selection);
+
+        if (isStrictColumns()) {
+            enforceStrictColumns(values);
+        }
+        if (isStrictGrammar()) {
+            enforceStrictGrammar(selection, null, null, null, null);
+        }
+        if (isStrict()) {
+            // Validate the user-supplied selection to detect syntactic anomalies
+            // in the selection string that could indicate a SQL injection attempt.
+            // The idea is to ensure that the selection clause is a valid SQL expression
+            // by compiling it twice: once wrapped in parentheses and once as
+            // originally specified. An attacker cannot create an expression that
+            // would escape the SQL expression while maintaining balanced parentheses
+            // in both the wrapped and original forms.
+
+            // NOTE: The ordering of the below operations is important; we must
+            // execute the wrapped query to ensure the untrusted clause has been
+            // fully isolated.
+
+            // Validate the unwrapped query
+            db.validateSql(unwrappedSql, null); // will throw if query is invalid
+
+            // Execute wrapped query for extra protection
+            final String wrappedSql = buildUpdate(values, wrap(selection));
+            sql = wrappedSql;
+        } else {
+            // Execute unwrapped query
+            sql = unwrappedSql;
+        }
+
+        if (selectionArgs == null) {
+            selectionArgs = EmptyArray.STRING;
+        }
+        final ArrayMap<String, Object> rawValues = values.getValues();
+        final int valuesLength = rawValues.size();
+        final Object[] sqlArgs = new Object[valuesLength + selectionArgs.length];
+        for (int i = 0; i < sqlArgs.length; i++) {
+            if (i < valuesLength) {
+                sqlArgs[i] = rawValues.valueAt(i);
+            } else {
+                sqlArgs[i] = selectionArgs[i - valuesLength];
+            }
+        }
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            if (Build.IS_DEBUGGABLE) {
+                Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
+            } else {
+                Log.d(TAG, sql);
+            }
+        }
+        return db.executeSql(sql, sqlArgs);
+    }
+
+    /**
+     * Perform a delete by combining all current settings and the
+     * information passed into this method.
+     *
+     * @param db the database to delete on
+     * @param selection A filter declaring which rows to return,
+     *   formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
+     *   itself). Passing null will return all rows for the given URL.
+     * @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.
+     * @return the number of rows deleted
+     */
+    public int delete(@NonNull SQLiteDatabase db, @Nullable String selection,
+            @Nullable String[] selectionArgs) {
+        Objects.requireNonNull(mTables, "No tables defined");
+        Objects.requireNonNull(db, "No database defined");
+
+        final String sql;
+        final String unwrappedSql = buildDelete(selection);
+
+        if (isStrictGrammar()) {
+            enforceStrictGrammar(selection, null, null, null, null);
+        }
+        if (isStrict()) {
+            // Validate the user-supplied selection to detect syntactic anomalies
+            // in the selection string that could indicate a SQL injection attempt.
+            // The idea is to ensure that the selection clause is a valid SQL expression
+            // by compiling it twice: once wrapped in parentheses and once as
+            // originally specified. An attacker cannot create an expression that
+            // would escape the SQL expression while maintaining balanced parentheses
+            // in both the wrapped and original forms.
+
+            // NOTE: The ordering of the below operations is important; we must
+            // execute the wrapped query to ensure the untrusted clause has been
+            // fully isolated.
+
+            // Validate the unwrapped query
+            db.validateSql(unwrappedSql, null); // will throw if query is invalid
+
+            // Execute wrapped query for extra protection
+            final String wrappedSql = buildDelete(wrap(selection));
+            sql = wrappedSql;
+        } else {
+            // Execute unwrapped query
+            sql = unwrappedSql;
+        }
+
+        final String[] sqlArgs = selectionArgs;
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            if (Build.IS_DEBUGGABLE) {
+                Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
+            } else {
+                Log.d(TAG, sql);
+            }
+        }
+        return db.executeSql(sql, sqlArgs);
+    }
+
+    private void enforceStrictColumns(@Nullable String[] projection) {
+        Objects.requireNonNull(mProjectionMap, "No projection map defined");
+
+        computeProjection(projection);
+    }
+
+    private void enforceStrictColumns(@NonNull ContentValues values) {
+        Objects.requireNonNull(mProjectionMap, "No projection map defined");
+
+        final ArrayMap<String, Object> rawValues = values.getValues();
+        for (int i = 0; i < rawValues.size(); i++) {
+            final String column = rawValues.keyAt(i);
+            if (!mProjectionMap.containsKey(column)) {
+                throw new IllegalArgumentException("Invalid column " + column);
+            }
+        }
+    }
+
+    private void enforceStrictGrammar(@Nullable String selection, @Nullable String groupBy,
+            @Nullable String having, @Nullable String sortOrder, @Nullable String limit) {
+        SQLiteTokenizer.tokenize(selection, SQLiteTokenizer.OPTION_NONE,
+                this::enforceStrictToken);
+        SQLiteTokenizer.tokenize(groupBy, SQLiteTokenizer.OPTION_NONE,
+                this::enforceStrictToken);
+        SQLiteTokenizer.tokenize(having, SQLiteTokenizer.OPTION_NONE,
+                this::enforceStrictToken);
+        SQLiteTokenizer.tokenize(sortOrder, SQLiteTokenizer.OPTION_NONE,
+                this::enforceStrictToken);
+        SQLiteTokenizer.tokenize(limit, SQLiteTokenizer.OPTION_NONE,
+                this::enforceStrictToken);
+    }
+
+    private void enforceStrictToken(@NonNull String token) {
+        if (isTableOrColumn(token)) return;
+        if (SQLiteTokenizer.isFunction(token)) return;
+        if (SQLiteTokenizer.isType(token)) return;
+
+        // Carefully block any tokens that are attempting to jump across query
+        // clauses or create subqueries, since they could leak data that should
+        // have been filtered by the trusted where clause
+        boolean isAllowedKeyword = SQLiteTokenizer.isKeyword(token);
+        switch (token.toUpperCase(Locale.US)) {
+            case "SELECT":
+            case "FROM":
+            case "WHERE":
+            case "GROUP":
+            case "HAVING":
+            case "WINDOW":
+            case "VALUES":
+            case "ORDER":
+            case "LIMIT":
+                isAllowedKeyword = false;
+                break;
+        }
+        if (!isAllowedKeyword) {
+            throw new IllegalArgumentException("Invalid token " + token);
+        }
+    }
+
+    /**
+     * Construct a {@code SELECT} statement suitable for use in a group of
+     * {@code SELECT} statements that will be joined through {@code UNION} operators
+     * in buildUnionQuery.
+     *
+     * @param projectionIn 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 {@code WHERE} clause (excluding the {@code WHERE}
+     *   itself).  Passing null will return all rows for the given
+     *   URL.
+     * @param groupBy A filter declaring how to group rows, formatted
+     *   as an SQL {@code GROUP BY} clause (excluding the {@code 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 {@code HAVING} clause (excluding the {@code HAVING} itself).  Passing
+     *   null will cause all row groups to be included, and is
+     *   required when row grouping is not being used.
+     * @param sortOrder How to order the rows, formatted as an SQL
+     *   {@code ORDER BY} clause (excluding the {@code 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 {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
+     * @return the resulting SQL {@code SELECT} statement
+     */
+    public String buildQuery(
+            String[] projectionIn, String selection, String groupBy,
+            String having, String sortOrder, String limit) {
+        String[] projection = computeProjection(projectionIn);
+        String where = computeWhere(selection);
+
+        return buildQueryString(
+                mDistinct, mTables, projection, where,
+                groupBy, having, sortOrder, limit);
+    }
+
+    /**
+     * @deprecated This method's signature is misleading since no SQL parameter
+     * substitution is carried out.  The selection arguments parameter does not get
+     * used at all.  To avoid confusion, call
+     * {@link #buildQuery(String[], String, String, String, String, String)} instead.
+     */
+    @Deprecated
+    public String buildQuery(
+            String[] projectionIn, String selection, String[] selectionArgs,
+            String groupBy, String having, String sortOrder, String limit) {
+        return buildQuery(projectionIn, selection, groupBy, having, sortOrder, limit);
+    }
+
+    /** {@hide} */
+    public String buildInsert(ContentValues values) {
+        if (values == null || values.isEmpty()) {
+            throw new IllegalArgumentException("Empty values");
+        }
+
+        StringBuilder sql = new StringBuilder(120);
+        sql.append("INSERT INTO ");
+        sql.append(SQLiteDatabase.findEditTable(mTables));
+        sql.append(" (");
+
+        final ArrayMap<String, Object> rawValues = values.getValues();
+        for (int i = 0; i < rawValues.size(); i++) {
+            if (i > 0) {
+                sql.append(',');
+            }
+            sql.append(rawValues.keyAt(i));
+        }
+        sql.append(") VALUES (");
+        for (int i = 0; i < rawValues.size(); i++) {
+            if (i > 0) {
+                sql.append(',');
+            }
+            sql.append('?');
+        }
+        sql.append(")");
+        return sql.toString();
+    }
+
+    /** {@hide} */
+    public String buildUpdate(ContentValues values, String selection) {
+        if (values == null || values.isEmpty()) {
+            throw new IllegalArgumentException("Empty values");
+        }
+
+        StringBuilder sql = new StringBuilder(120);
+        sql.append("UPDATE ");
+        sql.append(SQLiteDatabase.findEditTable(mTables));
+        sql.append(" SET ");
+
+        final ArrayMap<String, Object> rawValues = values.getValues();
+        for (int i = 0; i < rawValues.size(); i++) {
+            if (i > 0) {
+                sql.append(',');
+            }
+            sql.append(rawValues.keyAt(i));
+            sql.append("=?");
+        }
+
+        final String where = computeWhere(selection);
+        appendClause(sql, " WHERE ", where);
+        return sql.toString();
+    }
+
+    /** {@hide} */
+    public String buildDelete(String selection) {
+        StringBuilder sql = new StringBuilder(120);
+        sql.append("DELETE FROM ");
+        sql.append(SQLiteDatabase.findEditTable(mTables));
+
+        final String where = computeWhere(selection);
+        appendClause(sql, " WHERE ", where);
+        return sql.toString();
+    }
+
+    /**
+     * Construct a {@code SELECT} statement suitable for use in a group of
+     * {@code SELECT} statements that will be joined through {@code UNION} operators
+     * in buildUnionQuery.
+     *
+     * @param typeDiscriminatorColumn the name of the result column
+     *   whose cells will contain the name of the table from which
+     *   each row was drawn.
+     * @param unionColumns the names of the columns to appear in the
+     *   result.  This may include columns that do not appear in the
+     *   table this {@code SELECT} is querying (i.e. mTables), but that do
+     *   appear in one of the other tables in the {@code UNION} query that we
+     *   are constructing.
+     * @param columnsPresentInTable a Set of the names of the columns
+     *   that appear in this table (i.e. in the table whose name is
+     *   mTables).  Since columns in unionColumns include columns that
+     *   appear only in other tables, we use this array to distinguish
+     *   which ones actually are present.  Other columns will have
+     *   NULL values for results from this subquery.
+     * @param computedColumnsOffset all columns in unionColumns before
+     *   this index are included under the assumption that they're
+     *   computed and therefore won't appear in columnsPresentInTable,
+     *   e.g. "date * 1000 as normalized_date"
+     * @param typeDiscriminatorValue the value used for the
+     *   type-discriminator column in this subquery
+     * @param selection A filter declaring which rows to return,
+     *   formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
+     *   itself).  Passing null will return all rows for the given
+     *   URL.
+     * @param groupBy A filter declaring how to group rows, formatted
+     *   as an SQL {@code GROUP BY} clause (excluding the {@code 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 {@code HAVING} clause (excluding the {@code HAVING} itself).  Passing
+     *   null will cause all row groups to be included, and is
+     *   required when row grouping is not being used.
+     * @return the resulting SQL {@code SELECT} statement
+     */
+    public String buildUnionSubQuery(
+            String typeDiscriminatorColumn,
+            String[] unionColumns,
+            Set<String> columnsPresentInTable,
+            int computedColumnsOffset,
+            String typeDiscriminatorValue,
+            String selection,
+            String groupBy,
+            String having) {
+        int unionColumnsCount = unionColumns.length;
+        String[] projectionIn = new String[unionColumnsCount];
+
+        for (int i = 0; i < unionColumnsCount; i++) {
+            String unionColumn = unionColumns[i];
+
+            if (unionColumn.equals(typeDiscriminatorColumn)) {
+                projectionIn[i] = "'" + typeDiscriminatorValue + "' AS "
+                        + typeDiscriminatorColumn;
+            } else if (i <= computedColumnsOffset
+                       || columnsPresentInTable.contains(unionColumn)) {
+                projectionIn[i] = unionColumn;
+            } else {
+                projectionIn[i] = "NULL AS " + unionColumn;
+            }
+        }
+        return buildQuery(
+                projectionIn, selection, groupBy, having,
+                null /* sortOrder */,
+                null /* limit */);
+    }
+
+    /**
+     * @deprecated This method's signature is misleading since no SQL parameter
+     * substitution is carried out.  The selection arguments parameter does not get
+     * used at all.  To avoid confusion, call
+     * {@link #buildUnionSubQuery}
+     * instead.
+     */
+    @Deprecated
+    public String buildUnionSubQuery(
+            String typeDiscriminatorColumn,
+            String[] unionColumns,
+            Set<String> columnsPresentInTable,
+            int computedColumnsOffset,
+            String typeDiscriminatorValue,
+            String selection,
+            String[] selectionArgs,
+            String groupBy,
+            String having) {
+        return buildUnionSubQuery(
+                typeDiscriminatorColumn, unionColumns, columnsPresentInTable,
+                computedColumnsOffset, typeDiscriminatorValue, selection,
+                groupBy, having);
+    }
+
+    /**
+     * Given a set of subqueries, all of which are {@code SELECT} statements,
+     * construct a query that returns the union of what those
+     * subqueries return.
+     * @param subQueries an array of SQL {@code SELECT} statements, all of
+     *   which must have the same columns as the same positions in
+     *   their results
+     * @param sortOrder How to order the rows, formatted as an SQL
+     *   {@code ORDER BY} clause (excluding the {@code ORDER BY} itself).  Passing
+     *   null will use the default sort order, which may be unordered.
+     * @param limit The limit clause, which applies to the entire union result set
+     *
+     * @return the resulting SQL {@code SELECT} statement
+     */
+    public String buildUnionQuery(String[] subQueries, String sortOrder, String limit) {
+        StringBuilder query = new StringBuilder(128);
+        int subQueryCount = subQueries.length;
+        String unionOperator = mDistinct ? " UNION " : " UNION ALL ";
+
+        for (int i = 0; i < subQueryCount; i++) {
+            if (i > 0) {
+                query.append(unionOperator);
+            }
+            query.append(subQueries[i]);
+        }
+        appendClause(query, " ORDER BY ", sortOrder);
+        appendClause(query, " LIMIT ", limit);
+        return query.toString();
+    }
+
+    private static @NonNull String maybeWithOperator(@Nullable String operator,
+            @NonNull String column) {
+        if (operator != null) {
+            return operator + "(" + column + ")";
+        } else {
+            return column;
+        }
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public @Nullable String[] computeProjection(@Nullable String[] projectionIn) {
+        if (!ArrayUtils.isEmpty(projectionIn)) {
+            String[] projectionOut = new String[projectionIn.length];
+            for (int i = 0; i < projectionIn.length; i++) {
+                projectionOut[i] = computeSingleProjectionOrThrow(projectionIn[i]);
+            }
+            return projectionOut;
+        } else if (mProjectionMap != null) {
+            // Return all columns in projection map.
+            Set<Entry<String, String>> entrySet = mProjectionMap.entrySet();
+            String[] projection = new String[entrySet.size()];
+            Iterator<Entry<String, String>> entryIter = entrySet.iterator();
+            int i = 0;
+
+            while (entryIter.hasNext()) {
+                Entry<String, String> entry = entryIter.next();
+
+                // Don't include the _count column when people ask for no projection.
+                if (entry.getKey().equals(BaseColumns._COUNT)) {
+                    continue;
+                }
+                projection[i++] = entry.getValue();
+            }
+            return projection;
+        }
+        return null;
+    }
+
+    private @NonNull String computeSingleProjectionOrThrow(@NonNull String userColumn) {
+        final String column = computeSingleProjection(userColumn);
+        if (column != null) {
+            return column;
+        } else {
+            throw new IllegalArgumentException("Invalid column " + userColumn);
+        }
+    }
+
+    private @Nullable String computeSingleProjection(@NonNull String userColumn) {
+        // When no mapping provided, anything goes
+        if (mProjectionMap == null) {
+            return userColumn;
+        }
+
+        String operator = null;
+        String column = mProjectionMap.get(userColumn);
+
+        // When no direct match found, look for aggregation
+        if (column == null) {
+            final Matcher matcher = sAggregationPattern.matcher(userColumn);
+            if (matcher.matches()) {
+                operator = matcher.group(1);
+                userColumn = matcher.group(2);
+                column = mProjectionMap.get(userColumn);
+            }
+        }
+
+        if (column != null) {
+            return maybeWithOperator(operator, column);
+        }
+
+        if (mStrictFlags == 0 &&
+                (userColumn.contains(" AS ") || userColumn.contains(" as "))) {
+            /* A column alias already exist */
+            return maybeWithOperator(operator, userColumn);
+        }
+
+        // If greylist is configured, we might be willing to let
+        // this custom column bypass our strict checks.
+        if (mProjectionGreylist != null) {
+            boolean match = false;
+            for (Pattern p : mProjectionGreylist) {
+                if (p.matcher(userColumn).matches()) {
+                    match = true;
+                    break;
+                }
+            }
+
+            if (match) {
+                Log.w(TAG, "Allowing abusive custom column: " + userColumn);
+                return maybeWithOperator(operator, userColumn);
+            }
+        }
+
+        return null;
+    }
+
+    private boolean isTableOrColumn(String token) {
+        if (mTables.equals(token)) return true;
+        return computeSingleProjection(token) != null;
+    }
+
+    /** {@hide} */
+    public @Nullable String computeWhere(@Nullable String selection) {
+        final boolean hasInternal = !TextUtils.isEmpty(mWhereClause);
+        final boolean hasExternal = !TextUtils.isEmpty(selection);
+
+        if (hasInternal || hasExternal) {
+            final StringBuilder where = new StringBuilder();
+            if (hasInternal) {
+                where.append('(').append(mWhereClause).append(')');
+            }
+            if (hasInternal && hasExternal) {
+                where.append(" AND ");
+            }
+            if (hasExternal) {
+                where.append('(').append(selection).append(')');
+            }
+            return where.toString();
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Wrap given argument in parenthesis, unless it's {@code null} or
+     * {@code ()}, in which case return it verbatim.
+     */
+    private @Nullable String wrap(@Nullable String arg) {
+        if (TextUtils.isEmpty(arg)) {
+            return arg;
+        } else {
+            return "(" + arg + ")";
+        }
+    }
+}
diff --git a/android/database/sqlite/SQLiteReadOnlyDatabaseException.java b/android/database/sqlite/SQLiteReadOnlyDatabaseException.java
new file mode 100644
index 0000000..5b633c6
--- /dev/null
+++ b/android/database/sqlite/SQLiteReadOnlyDatabaseException.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2010 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 android.database.sqlite;
+
+public class SQLiteReadOnlyDatabaseException extends SQLiteException {
+    public SQLiteReadOnlyDatabaseException() {}
+
+    public SQLiteReadOnlyDatabaseException(String error) {
+        super(error);
+    }
+}
diff --git a/android/database/sqlite/SQLiteSession.java b/android/database/sqlite/SQLiteSession.java
new file mode 100644
index 0000000..24b62b8
--- /dev/null
+++ b/android/database/sqlite/SQLiteSession.java
@@ -0,0 +1,965 @@
+/*
+ * Copyright (C) 2011 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 android.database.sqlite;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.database.CursorWindow;
+import android.database.DatabaseUtils;
+import android.os.CancellationSignal;
+import android.os.OperationCanceledException;
+import android.os.ParcelFileDescriptor;
+
+/**
+ * Provides a single client the ability to use a database.
+ *
+ * <h2>About database sessions</h2>
+ * <p>
+ * Database access is always performed using a session.  The session
+ * manages the lifecycle of transactions and database connections.
+ * </p><p>
+ * Sessions can be used to perform both read-only and read-write operations.
+ * There is some advantage to knowing when a session is being used for
+ * read-only purposes because the connection pool can optimize the use
+ * of the available connections to permit multiple read-only operations
+ * to execute in parallel whereas read-write operations may need to be serialized.
+ * </p><p>
+ * When <em>Write Ahead Logging (WAL)</em> is enabled, the database can
+ * execute simultaneous read-only and read-write transactions, provided that
+ * at most one read-write transaction is performed at a time.  When WAL is not
+ * enabled, read-only transactions can execute in parallel but read-write
+ * transactions are mutually exclusive.
+ * </p>
+ *
+ * <h2>Ownership and concurrency guarantees</h2>
+ * <p>
+ * Session objects are not thread-safe.  In fact, session objects are thread-bound.
+ * The {@link SQLiteDatabase} uses a thread-local variable to associate a session
+ * with each thread for the use of that thread alone.  Consequently, each thread
+ * has its own session object and therefore its own transaction state independent
+ * of other threads.
+ * </p><p>
+ * A thread has at most one session per database.  This constraint ensures that
+ * a thread can never use more than one database connection at a time for a
+ * given database.  As the number of available database connections is limited,
+ * if a single thread tried to acquire multiple connections for the same database
+ * at the same time, it might deadlock.  Therefore we allow there to be only
+ * one session (so, at most one connection) per thread per database.
+ * </p>
+ *
+ * <h2>Transactions</h2>
+ * <p>
+ * There are two kinds of transaction: implicit transactions and explicit
+ * transactions.
+ * </p><p>
+ * An implicit transaction is created whenever a database operation is requested
+ * and there is no explicit transaction currently in progress.  An implicit transaction
+ * only lasts for the duration of the database operation in question and then it
+ * is ended.  If the database operation was successful, then its changes are committed.
+ * </p><p>
+ * An explicit transaction is started by calling {@link #beginTransaction} and
+ * specifying the desired transaction mode.  Once an explicit transaction has begun,
+ * all subsequent database operations will be performed as part of that transaction.
+ * To end an explicit transaction, first call {@link #setTransactionSuccessful} if the
+ * transaction was successful, then call {@link #end}.  If the transaction was
+ * marked successful, its changes will be committed, otherwise they will be rolled back.
+ * </p><p>
+ * Explicit transactions can also be nested.  A nested explicit transaction is
+ * started with {@link #beginTransaction}, marked successful with
+ * {@link #setTransactionSuccessful}and ended with {@link #endTransaction}.
+ * If any nested transaction is not marked successful, then the entire transaction
+ * including all of its nested transactions will be rolled back
+ * when the outermost transaction is ended.
+ * </p><p>
+ * To improve concurrency, an explicit transaction can be yielded by calling
+ * {@link #yieldTransaction}.  If there is contention for use of the database,
+ * then yielding ends the current transaction, commits its changes, releases the
+ * database connection for use by another session for a little while, and starts a
+ * new transaction with the same properties as the original one.
+ * Changes committed by {@link #yieldTransaction} cannot be rolled back.
+ * </p><p>
+ * When a transaction is started, the client can provide a {@link SQLiteTransactionListener}
+ * to listen for notifications of transaction-related events.
+ * </p><p>
+ * Recommended usage:
+ * <code><pre>
+ * // First, begin the transaction.
+ * session.beginTransaction(SQLiteSession.TRANSACTION_MODE_DEFERRED, 0);
+ * try {
+ *     // Then do stuff...
+ *     session.execute("INSERT INTO ...", null, 0);
+ *
+ *     // As the very last step before ending the transaction, mark it successful.
+ *     session.setTransactionSuccessful();
+ * } finally {
+ *     // Finally, end the transaction.
+ *     // This statement will commit the transaction if it was marked successful or
+ *     // roll it back otherwise.
+ *     session.endTransaction();
+ * }
+ * </pre></code>
+ * </p>
+ *
+ * <h2>Database connections</h2>
+ * <p>
+ * A {@link SQLiteDatabase} can have multiple active sessions at the same
+ * time.  Each session acquires and releases connections to the database
+ * as needed to perform each requested database transaction.  If all connections
+ * are in use, then database transactions on some sessions will block until a
+ * connection becomes available.
+ * </p><p>
+ * The session acquires a single database connection only for the duration
+ * of a single (implicit or explicit) database transaction, then releases it.
+ * This characteristic allows a small pool of database connections to be shared
+ * efficiently by multiple sessions as long as they are not all trying to perform
+ * database transactions at the same time.
+ * </p>
+ *
+ * <h2>Responsiveness</h2>
+ * <p>
+ * Because there are a limited number of database connections and the session holds
+ * a database connection for the entire duration of a database transaction,
+ * it is important to keep transactions short.  This is especially important
+ * for read-write transactions since they may block other transactions
+ * from executing.  Consider calling {@link #yieldTransaction} periodically
+ * during long-running transactions.
+ * </p><p>
+ * Another important consideration is that transactions that take too long to
+ * run may cause the application UI to become unresponsive.  Even if the transaction
+ * is executed in a background thread, the user will get bored and
+ * frustrated if the application shows no data for several seconds while
+ * a transaction runs.
+ * </p><p>
+ * Guidelines:
+ * <ul>
+ * <li>Do not perform database transactions on the UI thread.</li>
+ * <li>Keep database transactions as short as possible.</li>
+ * <li>Simple queries often run faster than complex queries.</li>
+ * <li>Measure the performance of your database transactions.</li>
+ * <li>Consider what will happen when the size of the data set grows.
+ * A query that works well on 100 rows may struggle with 10,000.</li>
+ * </ul>
+ *
+ * <h2>Reentrance</h2>
+ * <p>
+ * This class must tolerate reentrant execution of SQLite operations because
+ * triggers may call custom SQLite functions that perform additional queries.
+ * </p>
+ *
+ * @hide
+ */
+public final class SQLiteSession {
+    private final SQLiteConnectionPool mConnectionPool;
+
+    private SQLiteConnection mConnection;
+    private int mConnectionFlags;
+    private int mConnectionUseCount;
+    private Transaction mTransactionPool;
+    private Transaction mTransactionStack;
+
+    /**
+     * Transaction mode: Deferred.
+     * <p>
+     * In a deferred transaction, no locks are acquired on the database
+     * until the first operation is performed.  If the first operation is
+     * read-only, then a <code>SHARED</code> lock is acquired, otherwise
+     * a <code>RESERVED</code> lock is acquired.
+     * </p><p>
+     * While holding a <code>SHARED</code> lock, this session is only allowed to
+     * read but other sessions are allowed to read or write.
+     * While holding a <code>RESERVED</code> lock, this session is allowed to read
+     * or write but other sessions are only allowed to read.
+     * </p><p>
+     * Because the lock is only acquired when needed in a deferred transaction,
+     * it is possible for another session to write to the database first before
+     * this session has a chance to do anything.
+     * </p><p>
+     * Corresponds to the SQLite <code>BEGIN DEFERRED</code> transaction mode.
+     * </p>
+     */
+    public static final int TRANSACTION_MODE_DEFERRED = 0;
+
+    /**
+     * Transaction mode: Immediate.
+     * <p>
+     * When an immediate transaction begins, the session acquires a
+     * <code>RESERVED</code> lock.
+     * </p><p>
+     * While holding a <code>RESERVED</code> lock, this session is allowed to read
+     * or write but other sessions are only allowed to read.
+     * </p><p>
+     * Corresponds to the SQLite <code>BEGIN IMMEDIATE</code> transaction mode.
+     * </p>
+     */
+    public static final int TRANSACTION_MODE_IMMEDIATE = 1;
+
+    /**
+     * Transaction mode: Exclusive.
+     * <p>
+     * When an exclusive transaction begins, the session acquires an
+     * <code>EXCLUSIVE</code> lock.
+     * </p><p>
+     * While holding an <code>EXCLUSIVE</code> lock, this session is allowed to read
+     * or write but no other sessions are allowed to access the database.
+     * </p><p>
+     * Corresponds to the SQLite <code>BEGIN EXCLUSIVE</code> transaction mode.
+     * </p>
+     */
+    public static final int TRANSACTION_MODE_EXCLUSIVE = 2;
+
+    /**
+     * Creates a session bound to the specified connection pool.
+     *
+     * @param connectionPool The connection pool.
+     */
+    public SQLiteSession(SQLiteConnectionPool connectionPool) {
+        if (connectionPool == null) {
+            throw new IllegalArgumentException("connectionPool must not be null");
+        }
+
+        mConnectionPool = connectionPool;
+    }
+
+    /**
+     * Returns true if the session has a transaction in progress.
+     *
+     * @return True if the session has a transaction in progress.
+     */
+    public boolean hasTransaction() {
+        return mTransactionStack != null;
+    }
+
+    /**
+     * Returns true if the session has a nested transaction in progress.
+     *
+     * @return True if the session has a nested transaction in progress.
+     */
+    public boolean hasNestedTransaction() {
+        return mTransactionStack != null && mTransactionStack.mParent != null;
+    }
+
+    /**
+     * Returns true if the session has an active database connection.
+     *
+     * @return True if the session has an active database connection.
+     */
+    public boolean hasConnection() {
+        return mConnection != null;
+    }
+
+    /**
+     * Begins a transaction.
+     * <p>
+     * Transactions may nest.  If the transaction is not in progress,
+     * then a database connection is obtained and a new transaction is started.
+     * Otherwise, a nested transaction is started.
+     * </p><p>
+     * Each call to {@link #beginTransaction} must be matched exactly by a call
+     * to {@link #endTransaction}.  To mark a transaction as successful,
+     * call {@link #setTransactionSuccessful} before calling {@link #endTransaction}.
+     * If the transaction is not successful, or if any of its nested
+     * transactions were not successful, then the entire transaction will
+     * be rolled back when the outermost transaction is ended.
+     * </p>
+     *
+     * @param transactionMode The transaction mode.  One of: {@link #TRANSACTION_MODE_DEFERRED},
+     * {@link #TRANSACTION_MODE_IMMEDIATE}, or {@link #TRANSACTION_MODE_EXCLUSIVE}.
+     * Ignored when creating a nested transaction.
+     * @param transactionListener The transaction listener, or null if none.
+     * @param connectionFlags The connection flags to use if a connection must be
+     * acquired by this operation.  Refer to {@link SQLiteConnectionPool}.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     *
+     * @throws IllegalStateException if {@link #setTransactionSuccessful} has already been
+     * called for the current transaction.
+     * @throws SQLiteException if an error occurs.
+     * @throws OperationCanceledException if the operation was canceled.
+     *
+     * @see #setTransactionSuccessful
+     * @see #yieldTransaction
+     * @see #endTransaction
+     */
+    @UnsupportedAppUsage
+    public void beginTransaction(int transactionMode,
+            SQLiteTransactionListener transactionListener, int connectionFlags,
+            CancellationSignal cancellationSignal) {
+        throwIfTransactionMarkedSuccessful();
+        beginTransactionUnchecked(transactionMode, transactionListener, connectionFlags,
+                cancellationSignal);
+    }
+
+    private void beginTransactionUnchecked(int transactionMode,
+            SQLiteTransactionListener transactionListener, int connectionFlags,
+            CancellationSignal cancellationSignal) {
+        if (cancellationSignal != null) {
+            cancellationSignal.throwIfCanceled();
+        }
+
+        if (mTransactionStack == null) {
+            acquireConnection(null, connectionFlags, cancellationSignal); // might throw
+        }
+        try {
+            // Set up the transaction such that we can back out safely
+            // in case we fail part way.
+            if (mTransactionStack == null) {
+                // Execute SQL might throw a runtime exception.
+                switch (transactionMode) {
+                    case TRANSACTION_MODE_IMMEDIATE:
+                        mConnection.execute("BEGIN IMMEDIATE;", null,
+                                cancellationSignal); // might throw
+                        break;
+                    case TRANSACTION_MODE_EXCLUSIVE:
+                        mConnection.execute("BEGIN EXCLUSIVE;", null,
+                                cancellationSignal); // might throw
+                        break;
+                    default:
+                        mConnection.execute("BEGIN;", null, cancellationSignal); // might throw
+                        break;
+                }
+            }
+
+            // Listener might throw a runtime exception.
+            if (transactionListener != null) {
+                try {
+                    transactionListener.onBegin(); // might throw
+                } catch (RuntimeException ex) {
+                    if (mTransactionStack == null) {
+                        mConnection.execute("ROLLBACK;", null, cancellationSignal); // might throw
+                    }
+                    throw ex;
+                }
+            }
+
+            // Bookkeeping can't throw, except an OOM, which is just too bad...
+            Transaction transaction = obtainTransaction(transactionMode, transactionListener);
+            transaction.mParent = mTransactionStack;
+            mTransactionStack = transaction;
+        } finally {
+            if (mTransactionStack == null) {
+                releaseConnection(); // might throw
+            }
+        }
+    }
+
+    /**
+     * Marks the current transaction as having completed successfully.
+     * <p>
+     * This method can be called at most once between {@link #beginTransaction} and
+     * {@link #endTransaction} to indicate that the changes made by the transaction should be
+     * committed.  If this method is not called, the changes will be rolled back
+     * when the transaction is ended.
+     * </p>
+     *
+     * @throws IllegalStateException if there is no current transaction, or if
+     * {@link #setTransactionSuccessful} has already been called for the current transaction.
+     *
+     * @see #beginTransaction
+     * @see #endTransaction
+     */
+    public void setTransactionSuccessful() {
+        throwIfNoTransaction();
+        throwIfTransactionMarkedSuccessful();
+
+        mTransactionStack.mMarkedSuccessful = true;
+    }
+
+    /**
+     * Ends the current transaction and commits or rolls back changes.
+     * <p>
+     * If this is the outermost transaction (not nested within any other
+     * transaction), then the changes are committed if {@link #setTransactionSuccessful}
+     * was called or rolled back otherwise.
+     * </p><p>
+     * This method must be called exactly once for each call to {@link #beginTransaction}.
+     * </p>
+     *
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     *
+     * @throws IllegalStateException if there is no current transaction.
+     * @throws SQLiteException if an error occurs.
+     * @throws OperationCanceledException if the operation was canceled.
+     *
+     * @see #beginTransaction
+     * @see #setTransactionSuccessful
+     * @see #yieldTransaction
+     */
+    public void endTransaction(CancellationSignal cancellationSignal) {
+        throwIfNoTransaction();
+        assert mConnection != null;
+
+        endTransactionUnchecked(cancellationSignal, false);
+    }
+
+    private void endTransactionUnchecked(CancellationSignal cancellationSignal, boolean yielding) {
+        if (cancellationSignal != null) {
+            cancellationSignal.throwIfCanceled();
+        }
+
+        final Transaction top = mTransactionStack;
+        boolean successful = (top.mMarkedSuccessful || yielding) && !top.mChildFailed;
+
+        RuntimeException listenerException = null;
+        final SQLiteTransactionListener listener = top.mListener;
+        if (listener != null) {
+            try {
+                if (successful) {
+                    listener.onCommit(); // might throw
+                } else {
+                    listener.onRollback(); // might throw
+                }
+            } catch (RuntimeException ex) {
+                listenerException = ex;
+                successful = false;
+            }
+        }
+
+        mTransactionStack = top.mParent;
+        recycleTransaction(top);
+
+        if (mTransactionStack != null) {
+            if (!successful) {
+                mTransactionStack.mChildFailed = true;
+            }
+        } else {
+            try {
+                if (successful) {
+                    mConnection.execute("COMMIT;", null, cancellationSignal); // might throw
+                } else {
+                    mConnection.execute("ROLLBACK;", null, cancellationSignal); // might throw
+                }
+            } finally {
+                releaseConnection(); // might throw
+            }
+        }
+
+        if (listenerException != null) {
+            throw listenerException;
+        }
+    }
+
+    /**
+     * Temporarily ends a transaction to let other threads have use of
+     * the database.  Begins a new transaction after a specified delay.
+     * <p>
+     * If there are other threads waiting to acquire connections,
+     * then the current transaction is committed and the database
+     * connection is released.  After a short delay, a new transaction
+     * is started.
+     * </p><p>
+     * The transaction is assumed to be successful so far.  Do not call
+     * {@link #setTransactionSuccessful()} before calling this method.
+     * This method will fail if the transaction has already been marked
+     * successful.
+     * </p><p>
+     * The changes that were committed by a yield cannot be rolled back later.
+     * </p><p>
+     * Before this method was called, there must already have been
+     * a transaction in progress.  When this method returns, there will
+     * still be a transaction in progress, either the same one as before
+     * or a new one if the transaction was actually yielded.
+     * </p><p>
+     * This method should not be called when there is a nested transaction
+     * in progress because it is not possible to yield a nested transaction.
+     * If <code>throwIfNested</code> is true, then attempting to yield
+     * a nested transaction will throw {@link IllegalStateException}, otherwise
+     * the method will return <code>false</code> in that case.
+     * </p><p>
+     * If there is no nested transaction in progress but a previous nested
+     * transaction failed, then the transaction is not yielded (because it
+     * must be rolled back) and this method returns <code>false</code>.
+     * </p>
+     *
+     * @param sleepAfterYieldDelayMillis A delay time to wait after yielding
+     * the database connection to allow other threads some time to run.
+     * If the value is less than or equal to zero, there will be no additional
+     * delay beyond the time it will take to begin a new transaction.
+     * @param throwIfUnsafe If true, then instead of returning false when no
+     * transaction is in progress, a nested transaction is in progress, or when
+     * the transaction has already been marked successful, throws {@link IllegalStateException}.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * @return True if the transaction was actually yielded.
+     *
+     * @throws IllegalStateException if <code>throwIfNested</code> is true and
+     * there is no current transaction, there is a nested transaction in progress or
+     * if {@link #setTransactionSuccessful} has already been called for the current transaction.
+     * @throws SQLiteException if an error occurs.
+     * @throws OperationCanceledException if the operation was canceled.
+     *
+     * @see #beginTransaction
+     * @see #endTransaction
+     */
+    public boolean yieldTransaction(long sleepAfterYieldDelayMillis, boolean throwIfUnsafe,
+            CancellationSignal cancellationSignal) {
+        if (throwIfUnsafe) {
+            throwIfNoTransaction();
+            throwIfTransactionMarkedSuccessful();
+            throwIfNestedTransaction();
+        } else {
+            if (mTransactionStack == null || mTransactionStack.mMarkedSuccessful
+                    || mTransactionStack.mParent != null) {
+                return false;
+            }
+        }
+        assert mConnection != null;
+
+        if (mTransactionStack.mChildFailed) {
+            return false;
+        }
+
+        return yieldTransactionUnchecked(sleepAfterYieldDelayMillis,
+                cancellationSignal); // might throw
+    }
+
+    private boolean yieldTransactionUnchecked(long sleepAfterYieldDelayMillis,
+            CancellationSignal cancellationSignal) {
+        if (cancellationSignal != null) {
+            cancellationSignal.throwIfCanceled();
+        }
+
+        if (!mConnectionPool.shouldYieldConnection(mConnection, mConnectionFlags)) {
+            return false;
+        }
+
+        final int transactionMode = mTransactionStack.mMode;
+        final SQLiteTransactionListener listener = mTransactionStack.mListener;
+        final int connectionFlags = mConnectionFlags;
+        endTransactionUnchecked(cancellationSignal, true); // might throw
+
+        if (sleepAfterYieldDelayMillis > 0) {
+            try {
+                Thread.sleep(sleepAfterYieldDelayMillis);
+            } catch (InterruptedException ex) {
+                // we have been interrupted, that's all we need to do
+            }
+        }
+
+        beginTransactionUnchecked(transactionMode, listener, connectionFlags,
+                cancellationSignal); // might throw
+        return true;
+    }
+
+    /**
+     * Prepares a statement for execution but does not bind its parameters or execute it.
+     * <p>
+     * This method can be used to check for syntax errors during compilation
+     * prior to execution of the statement.  If the {@code outStatementInfo} argument
+     * is not null, the provided {@link SQLiteStatementInfo} object is populated
+     * with information about the statement.
+     * </p><p>
+     * A prepared statement makes no reference to the arguments that may eventually
+     * be bound to it, consequently it it possible to cache certain prepared statements
+     * such as SELECT or INSERT/UPDATE statements.  If the statement is cacheable,
+     * then it will be stored in the cache for later and reused if possible.
+     * </p>
+     *
+     * @param sql The SQL statement to prepare.
+     * @param connectionFlags The connection flags to use if a connection must be
+     * acquired by this operation.  Refer to {@link SQLiteConnectionPool}.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * @param outStatementInfo The {@link SQLiteStatementInfo} object to populate
+     * with information about the statement, or null if none.
+     *
+     * @throws SQLiteException if an error occurs, such as a syntax error.
+     * @throws OperationCanceledException if the operation was canceled.
+     */
+    public void prepare(String sql, int connectionFlags, CancellationSignal cancellationSignal,
+            SQLiteStatementInfo outStatementInfo) {
+        if (sql == null) {
+            throw new IllegalArgumentException("sql must not be null.");
+        }
+
+        if (cancellationSignal != null) {
+            cancellationSignal.throwIfCanceled();
+        }
+
+        acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
+        try {
+            mConnection.prepare(sql, outStatementInfo); // might throw
+        } finally {
+            releaseConnection(); // might throw
+        }
+    }
+
+    /**
+     * Executes a statement that does not return a result.
+     *
+     * @param sql The SQL statement to execute.
+     * @param bindArgs The arguments to bind, or null if none.
+     * @param connectionFlags The connection flags to use if a connection must be
+     * acquired by this operation.  Refer to {@link SQLiteConnectionPool}.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     *
+     * @throws SQLiteException if an error occurs, such as a syntax error
+     * or invalid number of bind arguments.
+     * @throws OperationCanceledException if the operation was canceled.
+     */
+    public void execute(String sql, Object[] bindArgs, int connectionFlags,
+            CancellationSignal cancellationSignal) {
+        if (sql == null) {
+            throw new IllegalArgumentException("sql must not be null.");
+        }
+
+        if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {
+            return;
+        }
+
+        acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
+        try {
+            mConnection.execute(sql, bindArgs, cancellationSignal); // might throw
+        } finally {
+            releaseConnection(); // might throw
+        }
+    }
+
+    /**
+     * Executes a statement that returns a single <code>long</code> result.
+     *
+     * @param sql The SQL statement to execute.
+     * @param bindArgs The arguments to bind, or null if none.
+     * @param connectionFlags The connection flags to use if a connection must be
+     * acquired by this operation.  Refer to {@link SQLiteConnectionPool}.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * @return The value of the first column in the first row of the result set
+     * as a <code>long</code>, or zero if none.
+     *
+     * @throws SQLiteException if an error occurs, such as a syntax error
+     * or invalid number of bind arguments.
+     * @throws OperationCanceledException if the operation was canceled.
+     */
+    public long executeForLong(String sql, Object[] bindArgs, int connectionFlags,
+            CancellationSignal cancellationSignal) {
+        if (sql == null) {
+            throw new IllegalArgumentException("sql must not be null.");
+        }
+
+        if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {
+            return 0;
+        }
+
+        acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
+        try {
+            return mConnection.executeForLong(sql, bindArgs, cancellationSignal); // might throw
+        } finally {
+            releaseConnection(); // might throw
+        }
+    }
+
+    /**
+     * Executes a statement that returns a single {@link String} result.
+     *
+     * @param sql The SQL statement to execute.
+     * @param bindArgs The arguments to bind, or null if none.
+     * @param connectionFlags The connection flags to use if a connection must be
+     * acquired by this operation.  Refer to {@link SQLiteConnectionPool}.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * @return The value of the first column in the first row of the result set
+     * as a <code>String</code>, or null if none.
+     *
+     * @throws SQLiteException if an error occurs, such as a syntax error
+     * or invalid number of bind arguments.
+     * @throws OperationCanceledException if the operation was canceled.
+     */
+    public String executeForString(String sql, Object[] bindArgs, int connectionFlags,
+            CancellationSignal cancellationSignal) {
+        if (sql == null) {
+            throw new IllegalArgumentException("sql must not be null.");
+        }
+
+        if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {
+            return null;
+        }
+
+        acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
+        try {
+            return mConnection.executeForString(sql, bindArgs, cancellationSignal); // might throw
+        } finally {
+            releaseConnection(); // might throw
+        }
+    }
+
+    /**
+     * Executes a statement that returns a single BLOB result as a
+     * file descriptor to a shared memory region.
+     *
+     * @param sql The SQL statement to execute.
+     * @param bindArgs The arguments to bind, or null if none.
+     * @param connectionFlags The connection flags to use if a connection must be
+     * acquired by this operation.  Refer to {@link SQLiteConnectionPool}.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * @return The file descriptor for a shared memory region that contains
+     * the value of the first column in the first row of the result set as a BLOB,
+     * or null if none.
+     *
+     * @throws SQLiteException if an error occurs, such as a syntax error
+     * or invalid number of bind arguments.
+     * @throws OperationCanceledException if the operation was canceled.
+     */
+    public ParcelFileDescriptor executeForBlobFileDescriptor(String sql, Object[] bindArgs,
+            int connectionFlags, CancellationSignal cancellationSignal) {
+        if (sql == null) {
+            throw new IllegalArgumentException("sql must not be null.");
+        }
+
+        if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {
+            return null;
+        }
+
+        acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
+        try {
+            return mConnection.executeForBlobFileDescriptor(sql, bindArgs,
+                    cancellationSignal); // might throw
+        } finally {
+            releaseConnection(); // might throw
+        }
+    }
+
+    /**
+     * Executes a statement that returns a count of the number of rows
+     * that were changed.  Use for UPDATE or DELETE SQL statements.
+     *
+     * @param sql The SQL statement to execute.
+     * @param bindArgs The arguments to bind, or null if none.
+     * @param connectionFlags The connection flags to use if a connection must be
+     * acquired by this operation.  Refer to {@link SQLiteConnectionPool}.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * @return The number of rows that were changed.
+     *
+     * @throws SQLiteException if an error occurs, such as a syntax error
+     * or invalid number of bind arguments.
+     * @throws OperationCanceledException if the operation was canceled.
+     */
+    public int executeForChangedRowCount(String sql, Object[] bindArgs, int connectionFlags,
+            CancellationSignal cancellationSignal) {
+        if (sql == null) {
+            throw new IllegalArgumentException("sql must not be null.");
+        }
+
+        if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {
+            return 0;
+        }
+
+        acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
+        try {
+            return mConnection.executeForChangedRowCount(sql, bindArgs,
+                    cancellationSignal); // might throw
+        } finally {
+            releaseConnection(); // might throw
+        }
+    }
+
+    /**
+     * Executes a statement that returns the row id of the last row inserted
+     * by the statement.  Use for INSERT SQL statements.
+     *
+     * @param sql The SQL statement to execute.
+     * @param bindArgs The arguments to bind, or null if none.
+     * @param connectionFlags The connection flags to use if a connection must be
+     * acquired by this operation.  Refer to {@link SQLiteConnectionPool}.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * @return The row id of the last row that was inserted, or 0 if none.
+     *
+     * @throws SQLiteException if an error occurs, such as a syntax error
+     * or invalid number of bind arguments.
+     * @throws OperationCanceledException if the operation was canceled.
+     */
+    public long executeForLastInsertedRowId(String sql, Object[] bindArgs, int connectionFlags,
+            CancellationSignal cancellationSignal) {
+        if (sql == null) {
+            throw new IllegalArgumentException("sql must not be null.");
+        }
+
+        if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {
+            return 0;
+        }
+
+        acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
+        try {
+            return mConnection.executeForLastInsertedRowId(sql, bindArgs,
+                    cancellationSignal); // might throw
+        } finally {
+            releaseConnection(); // might throw
+        }
+    }
+
+    /**
+     * Executes a statement and populates the specified {@link CursorWindow}
+     * with a range of results.  Returns the number of rows that were counted
+     * during query execution.
+     *
+     * @param sql The SQL statement to execute.
+     * @param bindArgs The arguments to bind, or null if none.
+     * @param window The cursor window to clear and fill.
+     * @param startPos The start position for filling the window.
+     * @param requiredPos The position of a row that MUST be in the window.
+     * If it won't fit, then the query should discard part of what it filled
+     * so that it does.  Must be greater than or equal to <code>startPos</code>.
+     * @param countAllRows True to count all rows that the query would return
+     * regagless of whether they fit in the window.
+     * @param connectionFlags The connection flags to use if a connection must be
+     * acquired by this operation.  Refer to {@link SQLiteConnectionPool}.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * @return The number of rows that were counted during query execution.  Might
+     * not be all rows in the result set unless <code>countAllRows</code> is true.
+     *
+     * @throws SQLiteException if an error occurs, such as a syntax error
+     * or invalid number of bind arguments.
+     * @throws OperationCanceledException if the operation was canceled.
+     */
+    public int executeForCursorWindow(String sql, Object[] bindArgs,
+            CursorWindow window, int startPos, int requiredPos, boolean countAllRows,
+            int connectionFlags, CancellationSignal cancellationSignal) {
+        if (sql == null) {
+            throw new IllegalArgumentException("sql must not be null.");
+        }
+        if (window == null) {
+            throw new IllegalArgumentException("window must not be null.");
+        }
+
+        if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {
+            window.clear();
+            return 0;
+        }
+
+        acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
+        try {
+            return mConnection.executeForCursorWindow(sql, bindArgs,
+                    window, startPos, requiredPos, countAllRows,
+                    cancellationSignal); // might throw
+        } finally {
+            releaseConnection(); // might throw
+        }
+    }
+
+    /**
+     * Performs special reinterpretation of certain SQL statements such as "BEGIN",
+     * "COMMIT" and "ROLLBACK" to ensure that transaction state invariants are
+     * maintained.
+     *
+     * This function is mainly used to support legacy apps that perform their
+     * own transactions by executing raw SQL rather than calling {@link #beginTransaction}
+     * and the like.
+     *
+     * @param sql The SQL statement to execute.
+     * @param bindArgs The arguments to bind, or null if none.
+     * @param connectionFlags The connection flags to use if a connection must be
+     * acquired by this operation.  Refer to {@link SQLiteConnectionPool}.
+     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+     * @return True if the statement was of a special form that was handled here,
+     * false otherwise.
+     *
+     * @throws SQLiteException if an error occurs, such as a syntax error
+     * or invalid number of bind arguments.
+     * @throws OperationCanceledException if the operation was canceled.
+     */
+    private boolean executeSpecial(String sql, Object[] bindArgs, int connectionFlags,
+            CancellationSignal cancellationSignal) {
+        if (cancellationSignal != null) {
+            cancellationSignal.throwIfCanceled();
+        }
+
+        final int type = DatabaseUtils.getSqlStatementType(sql);
+        switch (type) {
+            case DatabaseUtils.STATEMENT_BEGIN:
+                beginTransaction(TRANSACTION_MODE_EXCLUSIVE, null, connectionFlags,
+                        cancellationSignal);
+                return true;
+
+            case DatabaseUtils.STATEMENT_COMMIT:
+                setTransactionSuccessful();
+                endTransaction(cancellationSignal);
+                return true;
+
+            case DatabaseUtils.STATEMENT_ABORT:
+                endTransaction(cancellationSignal);
+                return true;
+        }
+        return false;
+    }
+
+    private void acquireConnection(String sql, int connectionFlags,
+            CancellationSignal cancellationSignal) {
+        if (mConnection == null) {
+            assert mConnectionUseCount == 0;
+            mConnection = mConnectionPool.acquireConnection(sql, connectionFlags,
+                    cancellationSignal); // might throw
+            mConnectionFlags = connectionFlags;
+        }
+        mConnectionUseCount += 1;
+    }
+
+    private void releaseConnection() {
+        assert mConnection != null;
+        assert mConnectionUseCount > 0;
+        if (--mConnectionUseCount == 0) {
+            try {
+                mConnectionPool.releaseConnection(mConnection); // might throw
+            } finally {
+                mConnection = null;
+            }
+        }
+    }
+
+    private void throwIfNoTransaction() {
+        if (mTransactionStack == null) {
+            throw new IllegalStateException("Cannot perform this operation because "
+                    + "there is no current transaction.");
+        }
+    }
+
+    private void throwIfTransactionMarkedSuccessful() {
+        if (mTransactionStack != null && mTransactionStack.mMarkedSuccessful) {
+            throw new IllegalStateException("Cannot perform this operation because "
+                    + "the transaction has already been marked successful.  The only "
+                    + "thing you can do now is call endTransaction().");
+        }
+    }
+
+    private void throwIfNestedTransaction() {
+        if (hasNestedTransaction()) {
+            throw new IllegalStateException("Cannot perform this operation because "
+                    + "a nested transaction is in progress.");
+        }
+    }
+
+    private Transaction obtainTransaction(int mode, SQLiteTransactionListener listener) {
+        Transaction transaction = mTransactionPool;
+        if (transaction != null) {
+            mTransactionPool = transaction.mParent;
+            transaction.mParent = null;
+            transaction.mMarkedSuccessful = false;
+            transaction.mChildFailed = false;
+        } else {
+            transaction = new Transaction();
+        }
+        transaction.mMode = mode;
+        transaction.mListener = listener;
+        return transaction;
+    }
+
+    private void recycleTransaction(Transaction transaction) {
+        transaction.mParent = mTransactionPool;
+        transaction.mListener = null;
+        mTransactionPool = transaction;
+    }
+
+    private static final class Transaction {
+        public Transaction mParent;
+        public int mMode;
+        public SQLiteTransactionListener mListener;
+        public boolean mMarkedSuccessful;
+        public boolean mChildFailed;
+    }
+}
diff --git a/android/database/sqlite/SQLiteStatement.java b/android/database/sqlite/SQLiteStatement.java
new file mode 100644
index 0000000..9fda8b0
--- /dev/null
+++ b/android/database/sqlite/SQLiteStatement.java
@@ -0,0 +1,165 @@
+/*
+ * 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 android.database.sqlite;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.ParcelFileDescriptor;
+
+/**
+ * Represents a statement that can be executed against a database.  The statement
+ * cannot return multiple rows or columns, but single value (1 x 1) result sets
+ * are supported.
+ * <p>
+ * This class is not thread-safe.
+ * </p>
+ */
+public final class SQLiteStatement extends SQLiteProgram {
+    @UnsupportedAppUsage
+    SQLiteStatement(SQLiteDatabase db, String sql, Object[] bindArgs) {
+        super(db, sql, bindArgs, null);
+    }
+
+    /**
+     * Execute this SQL statement, if it is not a SELECT / INSERT / DELETE / UPDATE, for example
+     * CREATE / DROP table, view, trigger, index etc.
+     *
+     * @throws android.database.SQLException If the SQL string is invalid for
+     *         some reason
+     */
+    public void execute() {
+        acquireReference();
+        try {
+            getSession().execute(getSql(), getBindArgs(), getConnectionFlags(), null);
+        } catch (SQLiteDatabaseCorruptException ex) {
+            onCorruption();
+            throw ex;
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Execute this SQL statement, if the number of rows affected by execution of this SQL
+     * statement is of any importance to the caller - for example, UPDATE / DELETE SQL statements.
+     *
+     * @return the number of rows affected by this SQL statement execution.
+     * @throws android.database.SQLException If the SQL string is invalid for
+     *         some reason
+     */
+    public int executeUpdateDelete() {
+        acquireReference();
+        try {
+            return getSession().executeForChangedRowCount(
+                    getSql(), getBindArgs(), getConnectionFlags(), null);
+        } catch (SQLiteDatabaseCorruptException ex) {
+            onCorruption();
+            throw ex;
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Execute this SQL statement and return the ID of the row inserted due to this call.
+     * The SQL statement should be an INSERT for this to be a useful call.
+     *
+     * @return the row ID of the last row inserted, if this insert is successful. -1 otherwise.
+     *
+     * @throws android.database.SQLException If the SQL string is invalid for
+     *         some reason
+     */
+    public long executeInsert() {
+        acquireReference();
+        try {
+            return getSession().executeForLastInsertedRowId(
+                    getSql(), getBindArgs(), getConnectionFlags(), null);
+        } catch (SQLiteDatabaseCorruptException ex) {
+            onCorruption();
+            throw ex;
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Execute a statement that returns a 1 by 1 table with a numeric value.
+     * For example, SELECT COUNT(*) FROM table;
+     *
+     * @return The result of the query.
+     *
+     * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows
+     */
+    public long simpleQueryForLong() {
+        acquireReference();
+        try {
+            return getSession().executeForLong(
+                    getSql(), getBindArgs(), getConnectionFlags(), null);
+        } catch (SQLiteDatabaseCorruptException ex) {
+            onCorruption();
+            throw ex;
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Execute a statement that returns a 1 by 1 table with a text value.
+     * For example, SELECT COUNT(*) FROM table;
+     *
+     * @return The result of the query.
+     *
+     * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows
+     */
+    public String simpleQueryForString() {
+        acquireReference();
+        try {
+            return getSession().executeForString(
+                    getSql(), getBindArgs(), getConnectionFlags(), null);
+        } catch (SQLiteDatabaseCorruptException ex) {
+            onCorruption();
+            throw ex;
+        } finally {
+            releaseReference();
+        }
+    }
+
+    /**
+     * Executes a statement that returns a 1 by 1 table with a blob value.
+     *
+     * @return A read-only file descriptor for a copy of the blob value, or {@code null}
+     *         if the value is null or could not be read for some reason.
+     *
+     * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows
+     */
+    public ParcelFileDescriptor simpleQueryForBlobFileDescriptor() {
+        acquireReference();
+        try {
+            return getSession().executeForBlobFileDescriptor(
+                    getSql(), getBindArgs(), getConnectionFlags(), null);
+        } catch (SQLiteDatabaseCorruptException ex) {
+            onCorruption();
+            throw ex;
+        } finally {
+            releaseReference();
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "SQLiteProgram: " + getSql();
+    }
+}
diff --git a/android/database/sqlite/SQLiteStatementInfo.java b/android/database/sqlite/SQLiteStatementInfo.java
new file mode 100644
index 0000000..3edfdb0
--- /dev/null
+++ b/android/database/sqlite/SQLiteStatementInfo.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2011 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 android.database.sqlite;
+
+/**
+ * Describes a SQLite statement.
+ *
+ * @hide
+ */
+public final class SQLiteStatementInfo {
+    /**
+     * The number of parameters that the statement has.
+     */
+    public int numParameters;
+
+    /**
+     * The names of all columns in the result set of the statement.
+     */
+    public String[] columnNames;
+
+    /**
+     * True if the statement is read-only.
+     */
+    public boolean readOnly;
+}
diff --git a/android/database/sqlite/SQLiteTableLockedException.java b/android/database/sqlite/SQLiteTableLockedException.java
new file mode 100644
index 0000000..8278df0
--- /dev/null
+++ b/android/database/sqlite/SQLiteTableLockedException.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2010 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 android.database.sqlite;
+
+public class SQLiteTableLockedException extends SQLiteException {
+    public SQLiteTableLockedException() {}
+
+    public SQLiteTableLockedException(String error) {
+        super(error);
+    }
+}
diff --git a/android/database/sqlite/SQLiteTokenizer.java b/android/database/sqlite/SQLiteTokenizer.java
new file mode 100644
index 0000000..7e7c3fb
--- /dev/null
+++ b/android/database/sqlite/SQLiteTokenizer.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2019 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 android.database.sqlite;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.function.Consumer;
+
+/**
+ * SQL Tokenizer specialized to extract tokens from SQL (snippets).
+ * <p>
+ * Based on sqlite3GetToken() in tokenzie.c in SQLite.
+ * <p>
+ * Source for v3.8.6 (which android uses): http://www.sqlite.org/src/artifact/ae45399d6252b4d7
+ * (Latest source as of now: http://www.sqlite.org/src/artifact/78c8085bc7af1922)
+ * <p>
+ * Also draft spec: http://www.sqlite.org/draft/tokenreq.html
+ *
+ * @hide
+ */
+public class SQLiteTokenizer {
+    private static boolean isAlpha(char ch) {
+        return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || (ch == '_');
+    }
+
+    private static boolean isNum(char ch) {
+        return ('0' <= ch && ch <= '9');
+    }
+
+    private static boolean isAlNum(char ch) {
+        return isAlpha(ch) || isNum(ch);
+    }
+
+    private static boolean isAnyOf(char ch, String set) {
+        return set.indexOf(ch) >= 0;
+    }
+
+    private static IllegalArgumentException genException(String message, String sql) {
+        throw new IllegalArgumentException(message + " in '" + sql + "'");
+    }
+
+    private static char peek(String s, int index) {
+        return index < s.length() ? s.charAt(index) : '\0';
+    }
+
+    public static final int OPTION_NONE = 0;
+
+    /**
+     * Require that SQL contains only tokens; any comments or values will result
+     * in an exception.
+     */
+    public static final int OPTION_TOKEN_ONLY = 1 << 0;
+
+    /**
+     * Tokenize the given SQL, returning the list of each encountered token.
+     *
+     * @throws IllegalArgumentException if invalid SQL is encountered.
+     */
+    public static List<String> tokenize(@Nullable String sql, int options) {
+        final ArrayList<String> res = new ArrayList<>();
+        tokenize(sql, options, res::add);
+        return res;
+    }
+
+    /**
+     * Tokenize the given SQL, sending each encountered token to the given
+     * {@link Consumer}.
+     *
+     * @throws IllegalArgumentException if invalid SQL is encountered.
+     */
+    public static void tokenize(@Nullable String sql, int options, Consumer<String> checker) {
+        if (sql == null) {
+            return;
+        }
+        int pos = 0;
+        final int len = sql.length();
+        while (pos < len) {
+            final char ch = peek(sql, pos);
+
+            // Regular token.
+            if (isAlpha(ch)) {
+                final int start = pos;
+                pos++;
+                while (isAlNum(peek(sql, pos))) {
+                    pos++;
+                }
+                final int end = pos;
+
+                final String token = sql.substring(start, end);
+                checker.accept(token);
+
+                continue;
+            }
+
+            // Handle quoted tokens
+            if (isAnyOf(ch, "'\"`")) {
+                final int quoteStart = pos;
+                pos++;
+
+                for (;;) {
+                    pos = sql.indexOf(ch, pos);
+                    if (pos < 0) {
+                        throw genException("Unterminated quote", sql);
+                    }
+                    if (peek(sql, pos + 1) != ch) {
+                        break;
+                    }
+                    // Quoted quote char -- e.g. "abc""def" is a single string.
+                    pos += 2;
+                }
+                final int quoteEnd = pos;
+                pos++;
+
+                if (ch != '\'') {
+                    // Extract the token
+                    final String tokenUnquoted = sql.substring(quoteStart + 1, quoteEnd);
+
+                    final String token;
+
+                    // Unquote if needed. i.e. "aa""bb" -> aa"bb
+                    if (tokenUnquoted.indexOf(ch) >= 0) {
+                        token = tokenUnquoted.replaceAll(
+                                String.valueOf(ch) + ch, String.valueOf(ch));
+                    } else {
+                        token = tokenUnquoted;
+                    }
+                    checker.accept(token);
+                } else {
+                    if ((options &= OPTION_TOKEN_ONLY) != 0) {
+                        throw genException("Non-token detected", sql);
+                    }
+                }
+                continue;
+            }
+            // Handle tokens enclosed in [...]
+            if (ch == '[') {
+                final int quoteStart = pos;
+                pos++;
+
+                pos = sql.indexOf(']', pos);
+                if (pos < 0) {
+                    throw genException("Unterminated quote", sql);
+                }
+                final int quoteEnd = pos;
+                pos++;
+
+                final String token = sql.substring(quoteStart + 1, quoteEnd);
+
+                checker.accept(token);
+                continue;
+            }
+            if ((options &= OPTION_TOKEN_ONLY) != 0) {
+                throw genException("Non-token detected", sql);
+            }
+
+            // Detect comments.
+            if (ch == '-' && peek(sql, pos + 1) == '-') {
+                pos += 2;
+                pos = sql.indexOf('\n', pos);
+                if (pos < 0) {
+                    // We disallow strings ending in an inline comment.
+                    throw genException("Unterminated comment", sql);
+                }
+                pos++;
+
+                continue;
+            }
+            if (ch == '/' && peek(sql, pos + 1) == '*') {
+                pos += 2;
+                pos = sql.indexOf("*/", pos);
+                if (pos < 0) {
+                    throw genException("Unterminated comment", sql);
+                }
+                pos += 2;
+
+                continue;
+            }
+
+            // Semicolon is never allowed.
+            if (ch == ';') {
+                throw genException("Semicolon is not allowed", sql);
+            }
+
+            // For this purpose, we can simply ignore other characters.
+            // (Note it doesn't handle the X'' literal properly and reports this X as a token,
+            // but that should be fine...)
+            pos++;
+        }
+    }
+
+    /**
+     * Test if given token is a
+     * <a href="https://www.sqlite.org/lang_keywords.html">SQLite reserved
+     * keyword</a>.
+     */
+    public static boolean isKeyword(@NonNull String token) {
+        switch (token.toUpperCase(Locale.US)) {
+            case "ABORT": case "ACTION": case "ADD": case "AFTER":
+            case "ALL": case "ALTER": case "ANALYZE": case "AND":
+            case "AS": case "ASC": case "ATTACH": case "AUTOINCREMENT":
+            case "BEFORE": case "BEGIN": case "BETWEEN": case "BINARY":
+            case "BY": case "CASCADE": case "CASE": case "CAST":
+            case "CHECK": case "COLLATE": case "COLUMN": case "COMMIT":
+            case "CONFLICT": case "CONSTRAINT": case "CREATE": case "CROSS":
+            case "CURRENT": case "CURRENT_DATE": case "CURRENT_TIME": case "CURRENT_TIMESTAMP":
+            case "DATABASE": case "DEFAULT": case "DEFERRABLE": case "DEFERRED":
+            case "DELETE": case "DESC": case "DETACH": case "DISTINCT":
+            case "DO": case "DROP": case "EACH": case "ELSE":
+            case "END": case "ESCAPE": case "EXCEPT": case "EXCLUDE":
+            case "EXCLUSIVE": case "EXISTS": case "EXPLAIN": case "FAIL":
+            case "FILTER": case "FOLLOWING": case "FOR": case "FOREIGN":
+            case "FROM": case "FULL": case "GLOB": case "GROUP":
+            case "GROUPS": case "HAVING": case "IF": case "IGNORE":
+            case "IMMEDIATE": case "IN": case "INDEX": case "INDEXED":
+            case "INITIALLY": case "INNER": case "INSERT": case "INSTEAD":
+            case "INTERSECT": case "INTO": case "IS": case "ISNULL":
+            case "JOIN": case "KEY": case "LEFT": case "LIKE":
+            case "LIMIT": case "MATCH": case "NATURAL": case "NO":
+            case "NOCASE": case "NOT": case "NOTHING": case "NOTNULL":
+            case "NULL": case "OF": case "OFFSET": case "ON":
+            case "OR": case "ORDER": case "OTHERS": case "OUTER":
+            case "OVER": case "PARTITION": case "PLAN": case "PRAGMA":
+            case "PRECEDING": case "PRIMARY": case "QUERY": case "RAISE":
+            case "RANGE": case "RECURSIVE": case "REFERENCES": case "REGEXP":
+            case "REINDEX": case "RELEASE": case "RENAME": case "REPLACE":
+            case "RESTRICT": case "RIGHT": case "ROLLBACK": case "ROW":
+            case "ROWS": case "RTRIM": case "SAVEPOINT": case "SELECT":
+            case "SET": case "TABLE": case "TEMP": case "TEMPORARY":
+            case "THEN": case "TIES": case "TO": case "TRANSACTION":
+            case "TRIGGER": case "UNBOUNDED": case "UNION": case "UNIQUE":
+            case "UPDATE": case "USING": case "VACUUM": case "VALUES":
+            case "VIEW": case "VIRTUAL": case "WHEN": case "WHERE":
+            case "WINDOW": case "WITH": case "WITHOUT":
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Test if given token is a
+     * <a href="https://www.sqlite.org/lang_corefunc.html">SQLite reserved
+     * function</a>.
+     */
+    public static boolean isFunction(@NonNull String token) {
+        switch (token.toLowerCase(Locale.US)) {
+            case "abs": case "avg": case "char": case "coalesce":
+            case "count": case "glob": case "group_concat": case "hex":
+            case "ifnull": case "instr": case "length": case "like":
+            case "likelihood": case "likely": case "lower": case "ltrim":
+            case "max": case "min": case "nullif": case "random":
+            case "randomblob": case "replace": case "round": case "rtrim":
+            case "substr": case "sum": case "total": case "trim":
+            case "typeof": case "unicode": case "unlikely": case "upper":
+            case "zeroblob":
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Test if given token is a
+     * <a href="https://www.sqlite.org/datatype3.html">SQLite reserved type</a>.
+     */
+    public static boolean isType(@NonNull String token) {
+        switch (token.toUpperCase(Locale.US)) {
+            case "INT": case "INTEGER": case "TINYINT": case "SMALLINT":
+            case "MEDIUMINT": case "BIGINT": case "INT2": case "INT8":
+            case "CHARACTER": case "VARCHAR": case "NCHAR": case "NVARCHAR":
+            case "TEXT": case "CLOB": case "BLOB": case "REAL":
+            case "DOUBLE": case "FLOAT": case "NUMERIC": case "DECIMAL":
+            case "BOOLEAN": case "DATE": case "DATETIME":
+                return true;
+            default:
+                return false;
+        }
+    }
+}
diff --git a/android/database/sqlite/SQLiteTransactionListener.java b/android/database/sqlite/SQLiteTransactionListener.java
new file mode 100644
index 0000000..f03b580
--- /dev/null
+++ b/android/database/sqlite/SQLiteTransactionListener.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2009 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 android.database.sqlite;
+
+/**
+ * A listener for transaction events.
+ */
+public interface SQLiteTransactionListener {
+    /**
+     * Called immediately after the transaction begins.
+     */
+    void onBegin();
+
+    /**
+     * Called immediately before commiting the transaction.
+     */
+    void onCommit();
+
+    /**
+     * Called if the transaction is about to be rolled back.
+     */
+    void onRollback();
+}
diff --git a/android/database/sqlite/SqliteWrapper.java b/android/database/sqlite/SqliteWrapper.java
new file mode 100644
index 0000000..f335fbd
--- /dev/null
+++ b/android/database/sqlite/SqliteWrapper.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 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 android.database.sqlite;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.util.Log;
+import android.widget.Toast;
+
+/**
+ * @hide
+ */
+
+public final class SqliteWrapper {
+    private static final String TAG = "SqliteWrapper";
+    private static final String SQLITE_EXCEPTION_DETAIL_MESSAGE
+                = "unable to open database file";
+
+    private SqliteWrapper() {
+        // Forbidden being instantiated.
+    }
+
+    // FIXME: need to optimize this method.
+    private static boolean isLowMemory(SQLiteException e) {
+        return e.getMessage().equals(SQLITE_EXCEPTION_DETAIL_MESSAGE);
+    }
+
+    @UnsupportedAppUsage
+    public static void checkSQLiteException(Context context, SQLiteException e) {
+        if (isLowMemory(e)) {
+            Toast.makeText(context, com.android.internal.R.string.low_memory,
+                    Toast.LENGTH_SHORT).show();
+        } else {
+            throw e;
+        }
+    }
+
+    @UnsupportedAppUsage
+    public static Cursor query(Context context, ContentResolver resolver, Uri uri,
+            String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+        try {
+            return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
+        } catch (SQLiteException e) {
+            Log.e(TAG, "Catch a SQLiteException when query: ", e);
+            checkSQLiteException(context, e);
+            return null;
+        }
+    }
+
+    public static boolean requery(Context context, Cursor cursor) {
+        try {
+            return cursor.requery();
+        } catch (SQLiteException e) {
+            Log.e(TAG, "Catch a SQLiteException when requery: ", e);
+            checkSQLiteException(context, e);
+            return false;
+        }
+    }
+    @UnsupportedAppUsage
+    public static int update(Context context, ContentResolver resolver, Uri uri,
+            ContentValues values, String where, String[] selectionArgs) {
+        try {
+            return resolver.update(uri, values, where, selectionArgs);
+        } catch (SQLiteException e) {
+            Log.e(TAG, "Catch a SQLiteException when update: ", e);
+            checkSQLiteException(context, e);
+            return -1;
+        }
+    }
+
+    @UnsupportedAppUsage
+    public static int delete(Context context, ContentResolver resolver, Uri uri,
+            String where, String[] selectionArgs) {
+        try {
+            return resolver.delete(uri, where, selectionArgs);
+        } catch (SQLiteException e) {
+            Log.e(TAG, "Catch a SQLiteException when delete: ", e);
+            checkSQLiteException(context, e);
+            return -1;
+        }
+    }
+
+    @UnsupportedAppUsage
+    public static Uri insert(Context context, ContentResolver resolver,
+            Uri uri, ContentValues values) {
+        try {
+            return resolver.insert(uri, values);
+        } catch (SQLiteException e) {
+            Log.e(TAG, "Catch a SQLiteException when insert: ", e);
+            checkSQLiteException(context, e);
+            return null;
+        }
+    }
+}