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 <= position <= 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 (<append chunk 1><append chunk2>) AND (<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 (<append chunk 1><append chunk2>) AND (<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;
+ }
+ }
+}