summaryrefslogtreecommitdiffstats
path: root/core/java/android/database
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:31:44 -0800
committerThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:31:44 -0800
commit9066cfe9886ac131c34d59ed0e2d287b0e3c0087 (patch)
treed88beb88001f2482911e3d28e43833b50e4b4e97 /core/java/android/database
parentd83a98f4ce9cfa908f5c54bbd70f03eec07e7553 (diff)
downloadframeworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.zip
frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.gz
frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.bz2
auto import from //depot/cupcake/@135843
Diffstat (limited to 'core/java/android/database')
-rw-r--r--core/java/android/database/AbstractCursor.java636
-rw-r--r--core/java/android/database/AbstractWindowedCursor.java204
-rw-r--r--core/java/android/database/BulkCursorNative.java440
-rw-r--r--core/java/android/database/BulkCursorToCursorAdaptor.java256
-rw-r--r--core/java/android/database/CharArrayBuffer.java33
-rw-r--r--core/java/android/database/ContentObservable.java56
-rw-r--r--core/java/android/database/ContentObserver.java138
-rw-r--r--core/java/android/database/CrossProcessCursor.java42
-rw-r--r--core/java/android/database/Cursor.java587
-rw-r--r--core/java/android/database/CursorIndexOutOfBoundsException.java31
-rw-r--r--core/java/android/database/CursorJoiner.java265
-rw-r--r--core/java/android/database/CursorToBulkCursorAdaptor.java233
-rw-r--r--core/java/android/database/CursorWindow.java483
-rw-r--r--core/java/android/database/CursorWrapper.java305
-rw-r--r--core/java/android/database/DataSetObservable.java47
-rw-r--r--core/java/android/database/DataSetObserver.java41
-rw-r--r--core/java/android/database/DatabaseUtils.java1018
-rw-r--r--core/java/android/database/IBulkCursor.java90
-rwxr-xr-xcore/java/android/database/IContentObserver.aidl31
-rw-r--r--core/java/android/database/MatrixCursor.java267
-rw-r--r--core/java/android/database/MergeCursor.java257
-rw-r--r--core/java/android/database/Observable.java78
-rw-r--r--core/java/android/database/SQLException.java30
-rw-r--r--core/java/android/database/StaleDataException.java34
-rw-r--r--core/java/android/database/package.html14
-rw-r--r--core/java/android/database/sqlite/SQLiteAbortException.java30
-rw-r--r--core/java/android/database/sqlite/SQLiteClosable.java55
-rw-r--r--core/java/android/database/sqlite/SQLiteConstraintException.java28
-rw-r--r--core/java/android/database/sqlite/SQLiteCursor.java606
-rw-r--r--core/java/android/database/sqlite/SQLiteCursorDriver.java58
-rw-r--r--core/java/android/database/sqlite/SQLiteDatabase.java1675
-rw-r--r--core/java/android/database/sqlite/SQLiteDatabaseCorruptException.java28
-rw-r--r--core/java/android/database/sqlite/SQLiteDebug.java107
-rw-r--r--core/java/android/database/sqlite/SQLiteDirectCursorDriver.java97
-rw-r--r--core/java/android/database/sqlite/SQLiteDiskIOException.java29
-rw-r--r--core/java/android/database/sqlite/SQLiteDoneException.java31
-rw-r--r--core/java/android/database/sqlite/SQLiteException.java30
-rw-r--r--core/java/android/database/sqlite/SQLiteFullException.java28
-rw-r--r--core/java/android/database/sqlite/SQLiteMisuseException.java25
-rw-r--r--core/java/android/database/sqlite/SQLiteOpenHelper.java229
-rw-r--r--core/java/android/database/sqlite/SQLiteProgram.java264
-rw-r--r--core/java/android/database/sqlite/SQLiteQuery.java194
-rw-r--r--core/java/android/database/sqlite/SQLiteQueryBuilder.java520
-rw-r--r--core/java/android/database/sqlite/SQLiteStatement.java168
-rw-r--r--core/java/android/database/sqlite/package.html20
45 files changed, 9838 insertions, 0 deletions
diff --git a/core/java/android/database/AbstractCursor.java b/core/java/android/database/AbstractCursor.java
new file mode 100644
index 0000000..76f0860
--- /dev/null
+++ b/core/java/android/database/AbstractCursor.java
@@ -0,0 +1,636 @@
+/*
+ * 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.content.ContentResolver;
+import android.net.Uri;
+import android.util.Config;
+import android.util.Log;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+import java.lang.ref.WeakReference;
+import java.lang.UnsupportedOperationException;
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * 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";
+
+ DataSetObservable mDataSetObservable = new DataSetObservable();
+ ContentObservable mContentObservable = new ContentObservable();
+
+ /* -------------------------------------------------------- */
+ /* These need to be implemented by subclasses */
+ abstract public int getCount();
+
+ abstract public String[] getColumnNames();
+
+ abstract public String getString(int column);
+ abstract public short getShort(int column);
+ abstract public int getInt(int column);
+ abstract public long getLong(int column);
+ abstract public float getFloat(int column);
+ abstract public double getDouble(int column);
+ abstract public boolean isNull(int column);
+
+ // TODO implement getBlob in all cursor types
+ public byte[] getBlob(int column) {
+ throw new UnsupportedOperationException("getBlob is not supported");
+ }
+ /* -------------------------------------------------------- */
+ /* Methods that may optionally be implemented by subclasses */
+
+ /**
+ * returns a pre-filled window, return NULL if no such window
+ */
+ public CursorWindow getWindow() {
+ return null;
+ }
+
+ public int getColumnCount() {
+ return getColumnNames().length;
+ }
+
+ public void deactivate() {
+ deactivateInternal();
+ }
+
+ /**
+ * @hide
+ */
+ public void deactivateInternal() {
+ if (mSelfObserver != null) {
+ mContentResolver.unregisterContentObserver(mSelfObserver);
+ mSelfObserverRegistered = false;
+ }
+ mDataSetObservable.notifyInvalidated();
+ }
+
+ public boolean requery() {
+ if (mSelfObserver != null && mSelfObserverRegistered == false) {
+ mContentResolver.registerContentObserver(mNotifyUri, true, mSelfObserver);
+ mSelfObserverRegistered = true;
+ }
+ mDataSetObservable.notifyChanged();
+ return true;
+ }
+
+ public boolean isClosed() {
+ return mClosed;
+ }
+
+ public void close() {
+ mClosed = true;
+ mContentObservable.unregisterAll();
+ deactivateInternal();
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean commitUpdates(Map<? extends Long,? extends Map<String,Object>> values) {
+ return false;
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean deleteRow() {
+ return false;
+ }
+
+ /**
+ * 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
+ */
+ public boolean onMove(int oldPosition, int newPosition) {
+ return true;
+ }
+
+
+ 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();
+ }
+ }
+
+ /* -------------------------------------------------------- */
+ /* Implementation */
+ public AbstractCursor() {
+ mPos = -1;
+ mRowIdColumnIndex = -1;
+ mCurrentRowID = null;
+ mUpdatedRows = new HashMap<Long, Map<String, Object>>();
+ }
+
+ public final int getPosition() {
+ return mPos;
+ }
+
+ 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;
+ if (mRowIdColumnIndex != -1) {
+ mCurrentRowID = Long.valueOf(getLong(mRowIdColumnIndex));
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Copy data from cursor to CursorWindow
+ * @param position start position of data
+ * @param window
+ */
+ public void fillWindow(int position, CursorWindow window) {
+ if (position < 0 || position > getCount()) {
+ return;
+ }
+ window.acquireReference();
+ try {
+ int oldpos = mPos;
+ mPos = position - 1;
+ window.clear();
+ window.setStartPosition(position);
+ int columnNum = getColumnCount();
+ window.setNumColumns(columnNum);
+ while (moveToNext() && window.allocRow()) {
+ for (int i = 0; i < columnNum; i++) {
+ String field = getString(i);
+ if (field != null) {
+ if (!window.putString(field, mPos, i)) {
+ window.freeLastRow();
+ break;
+ }
+ } else {
+ if (!window.putNull(mPos, i)) {
+ window.freeLastRow();
+ break;
+ }
+ }
+ }
+ }
+
+ mPos = oldpos;
+ } catch (IllegalStateException e){
+ // simply ignore it
+ } finally {
+ window.releaseReference();
+ }
+ }
+
+ public final boolean move(int offset) {
+ return moveToPosition(mPos + offset);
+ }
+
+ public final boolean moveToFirst() {
+ return moveToPosition(0);
+ }
+
+ public final boolean moveToLast() {
+ return moveToPosition(getCount() - 1);
+ }
+
+ public final boolean moveToNext() {
+ return moveToPosition(mPos + 1);
+ }
+
+ public final boolean moveToPrevious() {
+ return moveToPosition(mPos - 1);
+ }
+
+ public final boolean isFirst() {
+ return mPos == 0 && getCount() != 0;
+ }
+
+ public final boolean isLast() {
+ int cnt = getCount();
+ return mPos == (cnt - 1) && cnt != 0;
+ }
+
+ public final boolean isBeforeFirst() {
+ if (getCount() == 0) {
+ return true;
+ }
+ return mPos == -1;
+ }
+
+ public final boolean isAfterLast() {
+ if (getCount() == 0) {
+ return true;
+ }
+ return mPos == getCount();
+ }
+
+ 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 (Config.LOGV) {
+ if (getCount() > 0) {
+ Log.w("AbstractCursor", "Unknown column " + columnName);
+ }
+ }
+ return -1;
+ }
+
+ public int getColumnIndexOrThrow(String columnName) {
+ final int index = getColumnIndex(columnName);
+ if (index < 0) {
+ throw new IllegalArgumentException("column '" + columnName + "' does not exist");
+ }
+ return index;
+ }
+
+ public String getColumnName(int columnIndex) {
+ return getColumnNames()[columnIndex];
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateBlob(int columnIndex, byte[] value) {
+ return update(columnIndex, value);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateString(int columnIndex, String value) {
+ return update(columnIndex, value);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateShort(int columnIndex, short value) {
+ return update(columnIndex, Short.valueOf(value));
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateInt(int columnIndex, int value) {
+ return update(columnIndex, Integer.valueOf(value));
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateLong(int columnIndex, long value) {
+ return update(columnIndex, Long.valueOf(value));
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateFloat(int columnIndex, float value) {
+ return update(columnIndex, Float.valueOf(value));
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateDouble(int columnIndex, double value) {
+ return update(columnIndex, Double.valueOf(value));
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateToNull(int columnIndex) {
+ return update(columnIndex, null);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean update(int columnIndex, Object obj) {
+ if (!supportsUpdates()) {
+ return false;
+ }
+
+ // Long.valueOf() returns null sometimes!
+// Long rowid = Long.valueOf(getLong(mRowIdColumnIndex));
+ Long rowid = new Long(getLong(mRowIdColumnIndex));
+ if (rowid == null) {
+ throw new IllegalStateException("null rowid. mRowIdColumnIndex = " + mRowIdColumnIndex);
+ }
+
+ synchronized(mUpdatedRows) {
+ Map<String, Object> row = mUpdatedRows.get(rowid);
+ if (row == null) {
+ row = new HashMap<String, Object>();
+ mUpdatedRows.put(rowid, row);
+ }
+ row.put(getColumnNames()[columnIndex], obj);
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns <code>true</code> if there are pending updates that have not yet been committed.
+ *
+ * @return <code>true</code> if there are pending updates that have not yet been committed.
+ * @hide
+ * @deprecated
+ */
+ public boolean hasUpdates() {
+ synchronized(mUpdatedRows) {
+ return mUpdatedRows.size() > 0;
+ }
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public void abortUpdates() {
+ synchronized(mUpdatedRows) {
+ mUpdatedRows.clear();
+ }
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean commitUpdates() {
+ return commitUpdates(null);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean supportsUpdates() {
+ return mRowIdColumnIndex != -1;
+ }
+
+ public void registerContentObserver(ContentObserver observer) {
+ mContentObservable.registerObserver(observer);
+ }
+
+ public void unregisterContentObserver(ContentObserver observer) {
+ // cursor will unregister all observers when it close
+ if (!mClosed) {
+ mContentObservable.unregisterObserver(observer);
+ }
+ }
+
+ /**
+ * @hide pending API council approval
+ */
+ protected void notifyDataSetChange() {
+ mDataSetObservable.notifyChanged();
+ }
+
+ /**
+ * @hide pending API council approval
+ */
+ protected DataSetObservable getDataSetObservable() {
+ return mDataSetObservable;
+
+ }
+ public void registerDataSetObserver(DataSetObserver observer) {
+ mDataSetObservable.registerObserver(observer);
+
+ }
+
+ 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);
+ if (mNotifyUri != null && selfChange) {
+ mContentResolver.notifyChange(mNotifyUri, 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.
+ */
+ public void setNotificationUri(ContentResolver cr, Uri notifyUri) {
+ synchronized (mSelfObserverLock) {
+ mNotifyUri = notifyUri;
+ mContentResolver = cr;
+ if (mSelfObserver != null) {
+ mContentResolver.unregisterContentObserver(mSelfObserver);
+ }
+ mSelfObserver = new SelfContentObserver(this);
+ mContentResolver.registerContentObserver(mNotifyUri, true, mSelfObserver);
+ mSelfObserverRegistered = true;
+ }
+ }
+
+ public boolean getWantsAllOnMoveCalls() {
+ return false;
+ }
+
+ public Bundle getExtras() {
+ return Bundle.EMPTY;
+ }
+
+ public Bundle respond(Bundle extras) {
+ return Bundle.EMPTY;
+ }
+
+ /**
+ * This function returns true if the field has been updated and is
+ * used in conjunction with {@link #getUpdatedField} to allow subclasses to
+ * support reading uncommitted updates. NOTE: This function and
+ * {@link #getUpdatedField} should be called together inside of a
+ * block synchronized on mUpdatedRows.
+ *
+ * @param columnIndex the column index of the field to check
+ * @return true if the field has been updated, false otherwise
+ */
+ protected boolean isFieldUpdated(int columnIndex) {
+ if (mRowIdColumnIndex != -1 && mUpdatedRows.size() > 0) {
+ Map<String, Object> updates = mUpdatedRows.get(mCurrentRowID);
+ if (updates != null && updates.containsKey(getColumnNames()[columnIndex])) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * This function returns the uncommitted updated value for the field
+ * at columnIndex. NOTE: This function and {@link #isFieldUpdated} should
+ * be called together inside of a block synchronized on mUpdatedRows.
+ *
+ * @param columnIndex the column index of the field to retrieve
+ * @return the updated value
+ */
+ protected Object getUpdatedField(int columnIndex) {
+ Map<String, Object> updates = mUpdatedRows.get(mCurrentRowID);
+ return updates.get(getColumnNames()[columnIndex]);
+ }
+
+ /**
+ * 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);
+ }
+ }
+
+ /**
+ * 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);
+ }
+ }
+ }
+
+ /**
+ * This HashMap contains a mapping from Long rowIDs to another Map
+ * that maps from String column names to new values. A NULL value means to
+ * remove an existing value, and all numeric values are in their class
+ * forms, i.e. Integer, Long, Float, etc.
+ */
+ protected HashMap<Long, Map<String, Object>> mUpdatedRows;
+
+ /**
+ * This must be set to the index of the row ID column by any
+ * subclass that wishes to support updates.
+ */
+ protected int mRowIdColumnIndex;
+
+ protected int mPos;
+ protected Long mCurrentRowID;
+ protected ContentResolver mContentResolver;
+ protected boolean mClosed = false;
+ private Uri mNotifyUri;
+ private ContentObserver mSelfObserver;
+ final private Object mSelfObserverLock = new Object();
+ private boolean mSelfObserverRegistered;
+}
diff --git a/core/java/android/database/AbstractWindowedCursor.java b/core/java/android/database/AbstractWindowedCursor.java
new file mode 100644
index 0000000..4ac0aef
--- /dev/null
+++ b/core/java/android/database/AbstractWindowedCursor.java
@@ -0,0 +1,204 @@
+/*
+ * 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 base class for Cursors that store their data in {@link CursorWindow}s.
+ */
+public abstract class AbstractWindowedCursor extends AbstractCursor
+{
+ @Override
+ public byte[] getBlob(int columnIndex)
+ {
+ checkPosition();
+
+ synchronized(mUpdatedRows) {
+ if (isFieldUpdated(columnIndex)) {
+ return (byte[])getUpdatedField(columnIndex);
+ }
+ }
+
+ return mWindow.getBlob(mPos, columnIndex);
+ }
+
+ @Override
+ public String getString(int columnIndex)
+ {
+ checkPosition();
+
+ synchronized(mUpdatedRows) {
+ if (isFieldUpdated(columnIndex)) {
+ return (String)getUpdatedField(columnIndex);
+ }
+ }
+
+ return mWindow.getString(mPos, columnIndex);
+ }
+
+ @Override
+ public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer)
+ {
+ checkPosition();
+
+ synchronized(mUpdatedRows) {
+ if (isFieldUpdated(columnIndex)) {
+ super.copyStringToBuffer(columnIndex, buffer);
+ }
+ }
+
+ mWindow.copyStringToBuffer(mPos, columnIndex, buffer);
+ }
+
+ @Override
+ public short getShort(int columnIndex)
+ {
+ checkPosition();
+
+ synchronized(mUpdatedRows) {
+ if (isFieldUpdated(columnIndex)) {
+ Number value = (Number)getUpdatedField(columnIndex);
+ return value.shortValue();
+ }
+ }
+
+ return mWindow.getShort(mPos, columnIndex);
+ }
+
+ @Override
+ public int getInt(int columnIndex)
+ {
+ checkPosition();
+
+ synchronized(mUpdatedRows) {
+ if (isFieldUpdated(columnIndex)) {
+ Number value = (Number)getUpdatedField(columnIndex);
+ return value.intValue();
+ }
+ }
+
+ return mWindow.getInt(mPos, columnIndex);
+ }
+
+ @Override
+ public long getLong(int columnIndex)
+ {
+ checkPosition();
+
+ synchronized(mUpdatedRows) {
+ if (isFieldUpdated(columnIndex)) {
+ Number value = (Number)getUpdatedField(columnIndex);
+ return value.longValue();
+ }
+ }
+
+ return mWindow.getLong(mPos, columnIndex);
+ }
+
+ @Override
+ public float getFloat(int columnIndex)
+ {
+ checkPosition();
+
+ synchronized(mUpdatedRows) {
+ if (isFieldUpdated(columnIndex)) {
+ Number value = (Number)getUpdatedField(columnIndex);
+ return value.floatValue();
+ }
+ }
+
+ return mWindow.getFloat(mPos, columnIndex);
+ }
+
+ @Override
+ public double getDouble(int columnIndex)
+ {
+ checkPosition();
+
+ synchronized(mUpdatedRows) {
+ if (isFieldUpdated(columnIndex)) {
+ Number value = (Number)getUpdatedField(columnIndex);
+ return value.doubleValue();
+ }
+ }
+
+ return mWindow.getDouble(mPos, columnIndex);
+ }
+
+ @Override
+ public boolean isNull(int columnIndex)
+ {
+ checkPosition();
+
+ synchronized(mUpdatedRows) {
+ if (isFieldUpdated(columnIndex)) {
+ return getUpdatedField(columnIndex) == null;
+ }
+ }
+
+ return mWindow.isNull(mPos, columnIndex);
+ }
+
+ public boolean isBlob(int columnIndex)
+ {
+ checkPosition();
+
+ synchronized(mUpdatedRows) {
+ if (isFieldUpdated(columnIndex)) {
+ Object object = getUpdatedField(columnIndex);
+ return object == null || object instanceof byte[];
+ }
+ }
+
+ return mWindow.isBlob(mPos, columnIndex);
+ }
+
+ @Override
+ protected void checkPosition()
+ {
+ super.checkPosition();
+
+ if (mWindow == null) {
+ throw new StaleDataException("Access closed cursor");
+ }
+ }
+
+ @Override
+ public CursorWindow getWindow() {
+ return mWindow;
+ }
+
+ /**
+ * Set a new cursor window to cursor, usually set a remote cursor window
+ * @param window cursor window
+ */
+ public void setWindow(CursorWindow window) {
+ if (mWindow != null) {
+ mWindow.close();
+ }
+ mWindow = window;
+ }
+
+ public boolean hasWindow() {
+ return mWindow != null;
+ }
+
+ /**
+ * This needs be updated in {@link #onMove} by subclasses, and
+ * needs to be set to NULL when the contents of the cursor change.
+ */
+ protected CursorWindow mWindow;
+}
diff --git a/core/java/android/database/BulkCursorNative.java b/core/java/android/database/BulkCursorNative.java
new file mode 100644
index 0000000..baa94d8
--- /dev/null
+++ b/core/java/android/database/BulkCursorNative.java
@@ -0,0 +1,440 @@
+/*
+ * 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.Binder;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Bundle;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 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);
+ if (window == null) {
+ reply.writeInt(0);
+ return true;
+ }
+ reply.writeNoException();
+ reply.writeInt(1);
+ window.writeToParcel(reply, 0);
+ return true;
+ }
+
+ case COUNT_TRANSACTION: {
+ data.enforceInterface(IBulkCursor.descriptor);
+ int count = count();
+ reply.writeNoException();
+ reply.writeInt(count);
+ return true;
+ }
+
+ case GET_COLUMN_NAMES_TRANSACTION: {
+ data.enforceInterface(IBulkCursor.descriptor);
+ String[] columnNames = getColumnNames();
+ reply.writeNoException();
+ reply.writeInt(columnNames.length);
+ int length = columnNames.length;
+ for (int i = 0; i < length; i++) {
+ reply.writeString(columnNames[i]);
+ }
+ 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());
+ CursorWindow window = CursorWindow.CREATOR.createFromParcel(data);
+ int count = requery(observer, window);
+ reply.writeNoException();
+ reply.writeInt(count);
+ reply.writeBundle(getExtras());
+ return true;
+ }
+
+ case UPDATE_ROWS_TRANSACTION: {
+ data.enforceInterface(IBulkCursor.descriptor);
+ // TODO - what ClassLoader should be passed to readHashMap?
+ // TODO - switch to Bundle
+ HashMap<Long, Map<String, Object>> values = data.readHashMap(null);
+ boolean result = updateRows(values);
+ reply.writeNoException();
+ reply.writeInt((result == true ? 1 : 0));
+ return true;
+ }
+
+ case DELETE_ROW_TRANSACTION: {
+ data.enforceInterface(IBulkCursor.descriptor);
+ int position = data.readInt();
+ boolean result = deleteRow(position);
+ reply.writeNoException();
+ reply.writeInt((result == true ? 1 : 0));
+ return true;
+ }
+
+ case ON_MOVE_TRANSACTION: {
+ data.enforceInterface(IBulkCursor.descriptor);
+ int position = data.readInt();
+ onMove(position);
+ reply.writeNoException();
+ return true;
+ }
+
+ case WANTS_ON_MOVE_TRANSACTION: {
+ data.enforceInterface(IBulkCursor.descriptor);
+ boolean result = getWantsAllOnMoveCalls();
+ reply.writeNoException();
+ reply.writeInt(result ? 1 : 0);
+ 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 {
+ private IBinder mRemote;
+ private Bundle mExtras;
+
+ public BulkCursorProxy(IBinder remote)
+ {
+ mRemote = remote;
+ mExtras = null;
+ }
+
+ public IBinder asBinder()
+ {
+ return mRemote;
+ }
+
+ public CursorWindow getWindow(int startPos) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ data.writeInt(startPos);
+
+ mRemote.transact(GET_CURSOR_WINDOW_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ CursorWindow window = null;
+ if (reply.readInt() == 1) {
+ window = CursorWindow.newFromParcel(reply);
+ }
+
+ data.recycle();
+ reply.recycle();
+
+ return window;
+ }
+
+ public void onMove(int position) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ data.writeInt(position);
+
+ mRemote.transact(ON_MOVE_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ data.recycle();
+ reply.recycle();
+ }
+
+ public int count() throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ boolean result = mRemote.transact(COUNT_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ int count;
+ if (result == false) {
+ count = -1;
+ } else {
+ count = reply.readInt();
+ }
+ data.recycle();
+ reply.recycle();
+ return count;
+ }
+
+ public String[] getColumnNames() throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ mRemote.transact(GET_COLUMN_NAMES_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ String[] columnNames = null;
+ int numColumns = reply.readInt();
+ columnNames = new String[numColumns];
+ for (int i = 0; i < numColumns; i++) {
+ columnNames[i] = reply.readString();
+ }
+
+ data.recycle();
+ reply.recycle();
+ return columnNames;
+ }
+
+ public void deactivate() throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ mRemote.transact(DEACTIVATE_TRANSACTION, data, reply, 0);
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ data.recycle();
+ reply.recycle();
+ }
+
+ public void close() throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ mRemote.transact(CLOSE_TRANSACTION, data, reply, 0);
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ data.recycle();
+ reply.recycle();
+ }
+
+ public int requery(IContentObserver observer, CursorWindow window) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ data.writeStrongInterface(observer);
+ window.writeToParcel(data, 0);
+
+ 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();
+ }
+
+ data.recycle();
+ reply.recycle();
+
+ return count;
+ }
+
+ public boolean updateRows(Map values) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ data.writeMap(values);
+
+ mRemote.transact(UPDATE_ROWS_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ boolean result = (reply.readInt() == 1 ? true : false);
+
+ data.recycle();
+ reply.recycle();
+
+ return result;
+ }
+
+ public boolean deleteRow(int position) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ data.writeInt(position);
+
+ mRemote.transact(DELETE_ROW_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ boolean result = (reply.readInt() == 1 ? true : false);
+
+ data.recycle();
+ reply.recycle();
+
+ return result;
+ }
+
+ public boolean getWantsAllOnMoveCalls() throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ mRemote.transact(WANTS_ON_MOVE_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ int result = reply.readInt();
+ data.recycle();
+ reply.recycle();
+ return result != 0;
+ }
+
+ public Bundle getExtras() throws RemoteException {
+ if (mExtras == null) {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ mRemote.transact(GET_EXTRAS_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ mExtras = reply.readBundle();
+ data.recycle();
+ reply.recycle();
+ }
+ return mExtras;
+ }
+
+ public Bundle respond(Bundle extras) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ data.writeBundle(extras);
+
+ mRemote.transact(RESPOND_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ Bundle returnExtras = reply.readBundle();
+ data.recycle();
+ reply.recycle();
+ return returnExtras;
+ }
+}
+
diff --git a/core/java/android/database/BulkCursorToCursorAdaptor.java b/core/java/android/database/BulkCursorToCursorAdaptor.java
new file mode 100644
index 0000000..c26810a
--- /dev/null
+++ b/core/java/android/database/BulkCursorToCursorAdaptor.java
@@ -0,0 +1,256 @@
+/*
+ * 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.RemoteException;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.util.Map;
+
+/**
+ * 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;
+ private IBulkCursor mBulkCursor;
+ private int mCount;
+ private String[] mColumns;
+ private boolean mWantsAllOnMoveCalls;
+
+ public void set(IBulkCursor bulkCursor) {
+ mBulkCursor = bulkCursor;
+
+ try {
+ mCount = mBulkCursor.count();
+ mWantsAllOnMoveCalls = mBulkCursor.getWantsAllOnMoveCalls();
+
+ // Search for the rowID column index and set it for our parent
+ mColumns = mBulkCursor.getColumnNames();
+ int length = mColumns.length;
+ for (int i = 0; i < length; i++) {
+ if (mColumns[i].equals("_id")) {
+ mRowIdColumnIndex = i;
+ break;
+ }
+ }
+ } catch (RemoteException ex) {
+ Log.e(TAG, "Setup failed because the remote process is dead");
+ }
+ }
+
+ /**
+ * 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 synchronized IContentObserver getObserver() {
+ if (mObserverBridge == null) {
+ mObserverBridge = new SelfContentObserver(this);
+ }
+ return mObserverBridge.getContentObserver();
+ }
+
+ @Override
+ public int getCount() {
+ return mCount;
+ }
+
+ @Override
+ public boolean onMove(int oldPosition, int newPosition) {
+ try {
+ // Make sure we have the proper window
+ if (mWindow != null) {
+ if (newPosition < mWindow.getStartPosition() ||
+ newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) {
+ mWindow = mBulkCursor.getWindow(newPosition);
+ } else if (mWantsAllOnMoveCalls) {
+ mBulkCursor.onMove(newPosition);
+ }
+ } else {
+ mWindow = mBulkCursor.getWindow(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();
+
+ try {
+ mBulkCursor.deactivate();
+ } catch (RemoteException ex) {
+ Log.w(TAG, "Remote process exception when deactivating");
+ }
+ mWindow = null;
+ }
+
+ @Override
+ public void close() {
+ super.close();
+ try {
+ mBulkCursor.close();
+ } catch (RemoteException ex) {
+ Log.w(TAG, "Remote process exception when closing");
+ }
+ mWindow = null;
+ }
+
+ @Override
+ public boolean requery() {
+ try {
+ int oldCount = mCount;
+ //TODO get the window from a pool somewhere to avoid creating the memory dealer
+ mCount = mBulkCursor.requery(getObserver(), new CursorWindow(
+ false /* the window will be accessed across processes */));
+ if (mCount != -1) {
+ mPos = -1;
+ mWindow = null;
+
+ // 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;
+ }
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ @Override
+ public boolean deleteRow() {
+ try {
+ boolean result = mBulkCursor.deleteRow(mPos);
+ if (result != false) {
+ // The window contains the old value, discard it
+ mWindow = null;
+
+ // Fix up the position
+ mCount = mBulkCursor.count();
+ if (mPos < mCount) {
+ int oldPos = mPos;
+ mPos = -1;
+ moveToPosition(oldPos);
+ } else {
+ mPos = mCount;
+ }
+
+ // Send the change notification
+ onChange(true);
+ }
+ return result;
+ } catch (RemoteException ex) {
+ Log.e(TAG, "Unable to delete row because the remote process is dead");
+ return false;
+ }
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ return mColumns;
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ @Override
+ public boolean commitUpdates(Map<? extends Long,
+ ? extends Map<String,Object>> additionalValues) {
+ if (!supportsUpdates()) {
+ Log.e(TAG, "commitUpdates not supported on this cursor, did you include the _id column?");
+ return false;
+ }
+
+ synchronized(mUpdatedRows) {
+ if (additionalValues != null) {
+ mUpdatedRows.putAll(additionalValues);
+ }
+
+ if (mUpdatedRows.size() <= 0) {
+ return false;
+ }
+
+ try {
+ boolean result = mBulkCursor.updateRows(mUpdatedRows);
+
+ if (result == true) {
+ mUpdatedRows.clear();
+
+ // Send the change notification
+ onChange(true);
+ }
+ return result;
+ } catch (RemoteException ex) {
+ Log.e(TAG, "Unable to commit updates because the remote process is dead");
+ return false;
+ }
+ }
+ }
+
+ @Override
+ public Bundle getExtras() {
+ 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) {
+ try {
+ return mBulkCursor.respond(extras);
+ } 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);
+ }
+ }
+}
+
diff --git a/core/java/android/database/CharArrayBuffer.java b/core/java/android/database/CharArrayBuffer.java
new file mode 100644
index 0000000..73781b7
--- /dev/null
+++ b/core/java/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/core/java/android/database/ContentObservable.java b/core/java/android/database/ContentObservable.java
new file mode 100644
index 0000000..8d7b7c5
--- /dev/null
+++ b/core/java/android/database/ContentObservable.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;
+
+/**
+ * A specialization of Observable for ContentObserver that provides methods for
+ * invoking the various callback methods of ContentObserver.
+ */
+public class ContentObservable extends Observable<ContentObserver> {
+
+ @Override
+ public void registerObserver(ContentObserver observer) {
+ super.registerObserver(observer);
+ }
+
+ /**
+ * invokes dispatchUpdate on each observer, unless the observer doesn't want
+ * self-notifications and the update is from a self-notification
+ * @param selfChange
+ */
+ public void dispatchChange(boolean selfChange) {
+ synchronized(mObservers) {
+ for (ContentObserver observer : mObservers) {
+ if (!selfChange || observer.deliverSelfNotifications()) {
+ observer.dispatchChange(selfChange);
+ }
+ }
+ }
+ }
+
+ /**
+ * invokes onChange on each observer
+ * @param selfChange
+ */
+ public void notifyChange(boolean selfChange) {
+ synchronized(mObservers) {
+ for (ContentObserver observer : mObservers) {
+ observer.onChange(selfChange);
+ }
+ }
+ }
+}
diff --git a/core/java/android/database/ContentObserver.java b/core/java/android/database/ContentObserver.java
new file mode 100644
index 0000000..3b829a3
--- /dev/null
+++ b/core/java/android/database/ContentObserver.java
@@ -0,0 +1,138 @@
+/*
+ * 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.os.Handler;
+
+/**
+ * Receives call backs for changes to content. Must be implemented by objects which are added
+ * to a {@link ContentObservable}.
+ */
+public abstract class ContentObserver {
+
+ private Transport mTransport;
+
+ // Protects mTransport
+ private Object lock = new Object();
+
+ /* package */ Handler mHandler;
+
+ private final class NotificationRunnable implements Runnable {
+
+ private boolean mSelf;
+
+ public NotificationRunnable(boolean self) {
+ mSelf = self;
+ }
+
+ public void run() {
+ ContentObserver.this.onChange(mSelf);
+ }
+ }
+
+ private static final class Transport extends IContentObserver.Stub {
+ ContentObserver mContentObserver;
+
+ public Transport(ContentObserver contentObserver) {
+ mContentObserver = contentObserver;
+ }
+
+ public boolean deliverSelfNotifications() {
+ ContentObserver contentObserver = mContentObserver;
+ if (contentObserver != null) {
+ return contentObserver.deliverSelfNotifications();
+ }
+ return false;
+ }
+
+ public void onChange(boolean selfChange) {
+ ContentObserver contentObserver = mContentObserver;
+ if (contentObserver != null) {
+ contentObserver.dispatchChange(selfChange);
+ }
+ }
+
+ public void releaseContentObserver() {
+ mContentObserver = null;
+ }
+ }
+
+ /**
+ * onChange() will happen on the provider Handler.
+ *
+ * @param handler The handler to run {@link #onChange} on.
+ */
+ public ContentObserver(Handler handler) {
+ mHandler = handler;
+ }
+
+ /**
+ * Gets access to the binder transport object. Not for public consumption.
+ *
+ * {@hide}
+ */
+ public IContentObserver getContentObserver() {
+ synchronized(lock) {
+ 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}
+ */
+ public IContentObserver releaseContentObserver() {
+ synchronized(lock) {
+ Transport oldTransport = mTransport;
+ if (oldTransport != null) {
+ oldTransport.releaseContentObserver();
+ mTransport = null;
+ }
+ return oldTransport;
+ }
+ }
+
+ /**
+ * Returns true if this observer is interested in notifications for changes
+ * made through the cursor the observer is registered with.
+ */
+ public boolean deliverSelfNotifications() {
+ return false;
+ }
+
+ /**
+ * This method is called when a change occurs to the cursor that
+ * is being observed.
+ *
+ * @param selfChange true if the update was caused by a call to <code>commit</code> on the
+ * cursor that is being observed.
+ */
+ public void onChange(boolean selfChange) {}
+
+ public final void dispatchChange(boolean selfChange) {
+ if (mHandler == null) {
+ onChange(selfChange);
+ } else {
+ mHandler.post(new NotificationRunnable(selfChange));
+ }
+ }
+}
diff --git a/core/java/android/database/CrossProcessCursor.java b/core/java/android/database/CrossProcessCursor.java
new file mode 100644
index 0000000..77ba3a5
--- /dev/null
+++ b/core/java/android/database/CrossProcessCursor.java
@@ -0,0 +1,42 @@
+/*
+ * 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;
+
+public interface CrossProcessCursor extends Cursor{
+ /**
+ * returns a pre-filled window, return NULL if no such window
+ */
+ CursorWindow getWindow();
+
+ /**
+ * copies cursor data into the window start at pos
+ */
+ void fillWindow(int pos, CursorWindow winow);
+
+ /**
+ * 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
+ */
+ boolean onMove(int oldPosition, int newPosition);
+
+}
diff --git a/core/java/android/database/Cursor.java b/core/java/android/database/Cursor.java
new file mode 100644
index 0000000..79178f4
--- /dev/null
+++ b/core/java/android/database/Cursor.java
@@ -0,0 +1,587 @@
+/*
+ * 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.content.ContentResolver;
+import android.net.Uri;
+import android.os.Bundle;
+
+import java.util.Map;
+
+/**
+ * This interface provides random read-write access to the result set returned
+ * by a database query.
+ */
+public interface Cursor {
+ /**
+ * Returns the numbers of rows in the cursor.
+ *
+ * @return the number of rows in the cursor.
+ */
+ int getCount();
+
+ /**
+ * Returns the current position of the cursor in the row set.
+ * The value is zero-based. When the row set is first returned the cursor
+ * will be at positon -1, which is before the first row. After the
+ * last row is returned another call to next() will leave the cursor past
+ * the last entry, at a position of count().
+ *
+ * @return the current cursor position.
+ */
+ int getPosition();
+
+ /**
+ * Move the cursor by a relative amount, forward or backward, from the
+ * current position. Positive offsets move forwards, negative offsets move
+ * backwards. If the final position is outside of the bounds of the result
+ * set then the resultant position will be pinned to -1 or count() depending
+ * on whether the value is off the front or end of the set, respectively.
+ *
+ * <p>This method will return true if the requested destination was
+ * reachable, otherwise, it returns false. For example, if the cursor is at
+ * currently on the second entry in the result set and move(-5) is called,
+ * the position will be pinned at -1, and false will be returned.
+ *
+ * @param offset the offset to be applied from the current position.
+ * @return whether the requested move fully succeeded.
+ */
+ boolean move(int offset);
+
+ /**
+ * Move the cursor to an absolute position. The valid
+ * range of values is -1 &lt;= position &lt;= count.
+ *
+ * <p>This method will return true if the request destination was reachable,
+ * otherwise, it returns false.
+ *
+ * @param position the zero-based position to move to.
+ * @return whether the requested move fully succeeded.
+ */
+ boolean moveToPosition(int position);
+
+ /**
+ * Move the cursor to the first row.
+ *
+ * <p>This method will return false if the cursor is empty.
+ *
+ * @return whether the move succeeded.
+ */
+ boolean moveToFirst();
+
+ /**
+ * Move the cursor to the last row.
+ *
+ * <p>This method will return false if the cursor is empty.
+ *
+ * @return whether the move succeeded.
+ */
+ boolean moveToLast();
+
+ /**
+ * Move the cursor to the next row.
+ *
+ * <p>This method will return false if the cursor is already past the
+ * last entry in the result set.
+ *
+ * @return whether the move succeeded.
+ */
+ boolean moveToNext();
+
+ /**
+ * Move the cursor to the previous row.
+ *
+ * <p>This method will return false if the cursor is already before the
+ * first entry in the result set.
+ *
+ * @return whether the move succeeded.
+ */
+ boolean moveToPrevious();
+
+ /**
+ * Returns whether the cursor is pointing to the first row.
+ *
+ * @return whether the cursor is pointing at the first entry.
+ */
+ boolean isFirst();
+
+ /**
+ * Returns whether the cursor is pointing to the last row.
+ *
+ * @return whether the cursor is pointing at the last entry.
+ */
+ boolean isLast();
+
+ /**
+ * Returns whether the cursor is pointing to the position before the first
+ * row.
+ *
+ * @return whether the cursor is before the first result.
+ */
+ boolean isBeforeFirst();
+
+ /**
+ * Returns whether the cursor is pointing to the position after the last
+ * row.
+ *
+ * @return whether the cursor is after the last result.
+ */
+ boolean isAfterLast();
+
+ /**
+ * Removes the row at the current cursor position from the underlying data
+ * store. After this method returns the cursor will be pointing to the row
+ * after the row that is deleted. This has the side effect of decrementing
+ * the result of count() by one.
+ * <p>
+ * The query must have the row ID column in its selection, otherwise this
+ * call will fail.
+ *
+ * @hide
+ * @return whether the record was successfully deleted.
+ * @deprecated use {@link ContentResolver#delete(Uri, String, String[])}
+ */
+ @Deprecated
+ boolean deleteRow();
+
+ /**
+ * 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>If the native content of that column is not blob exception may throw
+ *
+ * @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>If the native content of that column is not text the result will be
+ * the result of passing the column value to String.valueOf(x).
+ *
+ * @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>If the native content of that column is not numeric the result will be
+ * the result of passing the column value to Short.valueOf(x).
+ *
+ * @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>If the native content of that column is not numeric the result will be
+ * the result of passing the column value to Integer.valueOf(x).
+ *
+ * @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>If the native content of that column is not numeric the result will be
+ * the result of passing the column value to Long.valueOf(x).
+ *
+ * @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>If the native content of that column is not numeric the result will be
+ * the result of passing the column value to Float.valueOf(x).
+ *
+ * @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>If the native content of that column is not numeric the result will be
+ * the result of passing the column value to Double.valueOf(x).
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as a double.
+ */
+ double getDouble(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);
+
+ /**
+ * Returns <code>true</code> if the cursor supports updates.
+ *
+ * @return whether the cursor supports updates.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean supportsUpdates();
+
+ /**
+ * Returns <code>true</code> if there are pending updates that have not yet been committed.
+ *
+ * @return <code>true</code> if there are pending updates that have not yet been committed.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean hasUpdates();
+
+ /**
+ * Updates the value for the given column in the row the cursor is
+ * currently pointing at. Updates are not committed to the backing store
+ * until {@link #commitUpdates()} is called.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @param value the new value.
+ * @return whether the operation succeeded.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean updateBlob(int columnIndex, byte[] value);
+
+ /**
+ * Updates the value for the given column in the row the cursor is
+ * currently pointing at. Updates are not committed to the backing store
+ * until {@link #commitUpdates()} is called.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @param value the new value.
+ * @return whether the operation succeeded.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean updateString(int columnIndex, String value);
+
+ /**
+ * Updates the value for the given column in the row the cursor is
+ * currently pointing at. Updates are not committed to the backing store
+ * until {@link #commitUpdates()} is called.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @param value the new value.
+ * @return whether the operation succeeded.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean updateShort(int columnIndex, short value);
+
+ /**
+ * Updates the value for the given column in the row the cursor is
+ * currently pointing at. Updates are not committed to the backing store
+ * until {@link #commitUpdates()} is called.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @param value the new value.
+ * @return whether the operation succeeded.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean updateInt(int columnIndex, int value);
+
+ /**
+ * Updates the value for the given column in the row the cursor is
+ * currently pointing at. Updates are not committed to the backing store
+ * until {@link #commitUpdates()} is called.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @param value the new value.
+ * @return whether the operation succeeded.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean updateLong(int columnIndex, long value);
+
+ /**
+ * Updates the value for the given column in the row the cursor is
+ * currently pointing at. Updates are not committed to the backing store
+ * until {@link #commitUpdates()} is called.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @param value the new value.
+ * @return whether the operation succeeded.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean updateFloat(int columnIndex, float value);
+
+ /**
+ * Updates the value for the given column in the row the cursor is
+ * currently pointing at. Updates are not committed to the backing store
+ * until {@link #commitUpdates()} is called.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @param value the new value.
+ * @return whether the operation succeeded.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean updateDouble(int columnIndex, double value);
+
+ /**
+ * Removes the value for the given column in the row the cursor is
+ * currently pointing at. Updates are not committed to the backing store
+ * until {@link #commitUpdates()} is called.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return whether the operation succeeded.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean updateToNull(int columnIndex);
+
+ /**
+ * Atomically commits all updates to the backing store. After completion,
+ * this method leaves the data in an inconsistent state and you should call
+ * {@link #requery} before reading data from the cursor again.
+ *
+ * @return whether the operation succeeded.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean commitUpdates();
+
+ /**
+ * Atomically commits all updates to the backing store, as well as the
+ * updates included in values. After completion,
+ * this method leaves the data in an inconsistent state and you should call
+ * {@link #requery} before reading data from the cursor again.
+ *
+ * @param values A map from row IDs to Maps associating column names with
+ * updated values. A null value indicates the field should be
+ removed.
+ * @return whether the operation succeeded.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean commitUpdates(Map<? extends Long,
+ ? extends Map<String,Object>> values);
+
+ /**
+ * Reverts all updates made to the cursor since the last call to
+ * commitUpdates.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ void abortUpdates();
+
+ /**
+ * 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.
+ */
+ 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}.
+ *
+ * @return true if the requery succeeded, false if not, in which case the
+ * cursor becomes invalid.
+ */
+ 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.
+ *
+ * @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);
+
+ /**
+ * 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();
+
+ /**
+ * 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 Bundle.EMTPY if there are no values. Never null.
+ */
+ 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 Bundle.EMTPY. Never null.
+ * @return extra values, or Bundle.EMTPY. Never null.
+ */
+ Bundle respond(Bundle extras);
+}
diff --git a/core/java/android/database/CursorIndexOutOfBoundsException.java b/core/java/android/database/CursorIndexOutOfBoundsException.java
new file mode 100644
index 0000000..1f77d00
--- /dev/null
+++ b/core/java/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/core/java/android/database/CursorJoiner.java b/core/java/android/database/CursorJoiner.java
new file mode 100644
index 0000000..e3c2988
--- /dev/null
+++ b/core/java/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 (CursorJointer.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/core/java/android/database/CursorToBulkCursorAdaptor.java b/core/java/android/database/CursorToBulkCursorAdaptor.java
new file mode 100644
index 0000000..19ad946
--- /dev/null
+++ b/core/java/android/database/CursorToBulkCursorAdaptor.java
@@ -0,0 +1,233 @@
+/*
+ * 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.database.sqlite.SQLiteMisuseException;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Config;
+import android.util.Log;
+
+import java.util.Map;
+
+
+/**
+ * Wraps a BulkCursor around an existing Cursor making it remotable.
+ *
+ * {@hide}
+ */
+public final class CursorToBulkCursorAdaptor extends BulkCursorNative
+ implements IBinder.DeathRecipient {
+ private static final String TAG = "Cursor";
+ private final CrossProcessCursor mCursor;
+ private CursorWindow mWindow;
+ private final String mProviderName;
+ private final boolean mReadOnly;
+ private ContentObserverProxy mObserver;
+
+ 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) {
+ try {
+ mRemote.onChange(selfChange);
+ } catch (RemoteException ex) {
+ // Do nothing, the far side is dead
+ }
+ }
+ }
+
+ public CursorToBulkCursorAdaptor(Cursor cursor, IContentObserver observer, String providerName,
+ boolean allowWrite, CursorWindow window) {
+ try {
+ mCursor = (CrossProcessCursor) cursor;
+ if (mCursor instanceof AbstractWindowedCursor) {
+ AbstractWindowedCursor windowedCursor = (AbstractWindowedCursor) cursor;
+ if (windowedCursor.hasWindow()) {
+ if (Log.isLoggable(TAG, Log.VERBOSE) || Config.LOGV) {
+ Log.v(TAG, "Cross process cursor has a local window before setWindow in "
+ + providerName, new RuntimeException());
+ }
+ }
+ windowedCursor.setWindow(window);
+ } else {
+ mWindow = window;
+ mCursor.fillWindow(0, window);
+ }
+ } catch (ClassCastException e) {
+ // TODO Implement this case.
+ throw new UnsupportedOperationException(
+ "Only CrossProcessCursor cursors are supported across process for now", e);
+ }
+ mProviderName = providerName;
+ mReadOnly = !allowWrite;
+
+ createAndRegisterObserverProxy(observer);
+ }
+
+ public void binderDied() {
+ mCursor.close();
+ if (mWindow != null) {
+ mWindow.close();
+ }
+ }
+
+ public CursorWindow getWindow(int startPos) {
+ mCursor.moveToPosition(startPos);
+
+ if (mWindow != null) {
+ if (startPos < mWindow.getStartPosition() ||
+ startPos >= (mWindow.getStartPosition() + mWindow.getNumRows())) {
+ mCursor.fillWindow(startPos, mWindow);
+ }
+ return mWindow;
+ } else {
+ return ((AbstractWindowedCursor)mCursor).getWindow();
+ }
+ }
+
+ public void onMove(int position) {
+ mCursor.onMove(mCursor.getPosition(), position);
+ }
+
+ public int count() {
+ return mCursor.getCount();
+ }
+
+ public String[] getColumnNames() {
+ return mCursor.getColumnNames();
+ }
+
+ public void deactivate() {
+ maybeUnregisterObserverProxy();
+ mCursor.deactivate();
+ }
+
+ public void close() {
+ maybeUnregisterObserverProxy();
+ mCursor.deactivate();
+
+ }
+
+ public int requery(IContentObserver observer, CursorWindow window) {
+ if (mWindow == null) {
+ ((AbstractWindowedCursor)mCursor).setWindow(window);
+ }
+ try {
+ if (!mCursor.requery()) {
+ return -1;
+ }
+ } catch (IllegalStateException e) {
+ IllegalStateException leakProgram = new IllegalStateException(
+ mProviderName + " Requery misuse db, mCursor isClosed:" +
+ mCursor.isClosed(), e);
+ throw leakProgram;
+ }
+
+ if (mWindow != null) {
+ mCursor.fillWindow(0, window);
+ mWindow = window;
+ }
+ maybeUnregisterObserverProxy();
+ createAndRegisterObserverProxy(observer);
+ return mCursor.getCount();
+ }
+
+ public boolean getWantsAllOnMoveCalls() {
+ return mCursor.getWantsAllOnMoveCalls();
+ }
+
+ /**
+ * 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 createAndRegisterObserverProxy(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 maybeUnregisterObserverProxy() {
+ if (mObserver != null) {
+ mCursor.unregisterContentObserver(mObserver);
+ mObserver.unlinkToDeath(this);
+ mObserver = null;
+ }
+ }
+
+ public boolean updateRows(Map<? extends Long, ? extends Map<String, Object>> values) {
+ if (mReadOnly) {
+ Log.w("ContentProvider", "Permission Denial: modifying "
+ + mProviderName
+ + " from pid=" + Binder.getCallingPid()
+ + ", uid=" + Binder.getCallingUid());
+ return false;
+ }
+ return mCursor.commitUpdates(values);
+ }
+
+ public boolean deleteRow(int position) {
+ if (mReadOnly) {
+ Log.w("ContentProvider", "Permission Denial: modifying "
+ + mProviderName
+ + " from pid=" + Binder.getCallingPid()
+ + ", uid=" + Binder.getCallingUid());
+ return false;
+ }
+ if (mCursor.moveToPosition(position) == false) {
+ return false;
+ }
+ return mCursor.deleteRow();
+ }
+
+ public Bundle getExtras() {
+ return mCursor.getExtras();
+ }
+
+ public Bundle respond(Bundle extras) {
+ return mCursor.respond(extras);
+ }
+}
diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java
new file mode 100644
index 0000000..8e26730
--- /dev/null
+++ b/core/java/android/database/CursorWindow.java
@@ -0,0 +1,483 @@
+/*
+ * 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.database.sqlite.SQLiteClosable;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A buffer containing multiple cursor rows.
+ */
+public class CursorWindow extends SQLiteClosable implements Parcelable {
+ /** The pointer to the native window class */
+ @SuppressWarnings("unused")
+ private int nWindow;
+
+ private int mStartPos;
+
+ /**
+ * Creates a new empty window.
+ *
+ * @param localWindow true if this window will be used in this process only
+ */
+ public CursorWindow(boolean localWindow) {
+ mStartPos = 0;
+ native_init(localWindow);
+ }
+
+ /**
+ * Returns the starting position of this window within the entire
+ * Cursor's result set.
+ *
+ * @return the starting position of this window within the entire
+ * Cursor's result set.
+ */
+ public int getStartPosition() {
+ return mStartPos;
+ }
+
+ /**
+ * Set the start position of cursor window
+ * @param pos
+ */
+ public void setStartPosition(int pos) {
+ mStartPos = pos;
+ }
+
+ /**
+ * Returns the number of rows in this window.
+ *
+ * @return the number of rows in this window.
+ */
+ public int getNumRows() {
+ acquireReference();
+ try {
+ return getNumRows_native();
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native int getNumRows_native();
+ /**
+ * Set number of Columns
+ * @param columnNum
+ * @return true if success
+ */
+ public boolean setNumColumns(int columnNum) {
+ acquireReference();
+ try {
+ return setNumColumns_native(columnNum);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native boolean setNumColumns_native(int columnNum);
+
+ /**
+ * Allocate a row in cursor window
+ * @return false if cursor window is out of memory
+ */
+ public boolean allocRow(){
+ acquireReference();
+ try {
+ return allocRow_native();
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native boolean allocRow_native();
+
+ /**
+ * Free the last row
+ */
+ public void freeLastRow(){
+ acquireReference();
+ try {
+ freeLastRow_native();
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native void freeLastRow_native();
+
+ /**
+ * copy byte array to cursor window
+ * @param value
+ * @param row
+ * @param col
+ * @return false if fail to copy
+ */
+ public boolean putBlob(byte[] value, int row, int col) {
+ acquireReference();
+ try {
+ return putBlob_native(value, row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native boolean putBlob_native(byte[] value, int row, int col);
+
+ /**
+ * Copy String to cursor window
+ * @param value
+ * @param row
+ * @param col
+ * @return false if fail to copy
+ */
+ public boolean putString(String value, int row, int col) {
+ acquireReference();
+ try {
+ return putString_native(value, row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native boolean putString_native(String value, int row, int col);
+
+ /**
+ * Copy integer to cursor window
+ * @param value
+ * @param row
+ * @param col
+ * @return false if fail to copy
+ */
+ public boolean putLong(long value, int row, int col) {
+ acquireReference();
+ try {
+ return putLong_native(value, row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native boolean putLong_native(long value, int row, int col);
+
+
+ /**
+ * Copy double to cursor window
+ * @param value
+ * @param row
+ * @param col
+ * @return false if fail to copy
+ */
+ public boolean putDouble(double value, int row, int col) {
+ acquireReference();
+ try {
+ return putDouble_native(value, row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native boolean putDouble_native(double value, int row, int col);
+
+ /**
+ * Set the [row, col] value to NULL
+ * @param row
+ * @param col
+ * @return false if fail to copy
+ */
+ public boolean putNull(int row, int col) {
+ acquireReference();
+ try {
+ return putNull_native(row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native boolean putNull_native(int row, int col);
+
+
+ /**
+ * Returns {@code true} if given field is {@code NULL}.
+ *
+ * @param row the row to read from, row - getStartPosition() being the actual row in the window
+ * @param col the column to read from
+ * @return {@code true} if given field is {@code NULL}
+ */
+ public boolean isNull(int row, int col) {
+ acquireReference();
+ try {
+ return isNull_native(row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native boolean isNull_native(int row, int col);
+
+ /**
+ * Returns a byte array for the given field.
+ *
+ * @param row the row to read from, row - getStartPosition() being the actual row in the window
+ * @param col the column to read from
+ * @return a String value for the given field
+ */
+ public byte[] getBlob(int row, int col) {
+ acquireReference();
+ try {
+ return getBlob_native(row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native byte[] getBlob_native(int row, int col);
+
+ /**
+ * Checks if a field contains either a blob or is null.
+ *
+ * @param row the row to read from, row - getStartPosition() being the actual row in the window
+ * @param col the column to read from
+ * @return {@code true} if given field is {@code NULL} or a blob
+ */
+ public boolean isBlob(int row, int col) {
+ acquireReference();
+ try {
+ return isBlob_native(row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native boolean isBlob_native(int row, int col);
+
+ /**
+ * Returns a String for the given field.
+ *
+ * @param row the row to read from, row - getStartPosition() being the actual row in the window
+ * @param col the column to read from
+ * @return a String value for the given field
+ */
+ public String getString(int row, int col) {
+ acquireReference();
+ try {
+ return getString_native(row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native String getString_native(int row, int col);
+
+ /**
+ * copy the text for the given field in the provided char array.
+ *
+ * @param row the row to read from, row - getStartPosition() being the actual row in the window
+ * @param col the column to read from
+ * @param buffer the CharArrayBuffer to copy the text into,
+ * If the requested string is larger than the buffer
+ * a new char buffer will be created to hold the string. and assigne to
+ * CharArrayBuffer.data
+ */
+ public void copyStringToBuffer(int row, int col, CharArrayBuffer buffer) {
+ if (buffer == null) {
+ throw new IllegalArgumentException("CharArrayBuffer should not be null");
+ }
+ if (buffer.data == null) {
+ buffer.data = new char[64];
+ }
+ acquireReference();
+ try {
+ char[] newbuf = copyStringToBuffer_native(
+ row - mStartPos, col, buffer.data.length, buffer);
+ if (newbuf != null) {
+ buffer.data = newbuf;
+ }
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native char[] copyStringToBuffer_native(
+ int row, int col, int bufferSize, CharArrayBuffer buffer);
+
+ /**
+ * Returns a long for the given field.
+ * row is 0 based
+ *
+ * @param row the row to read from, row - getStartPosition() being the actual row in the window
+ * @param col the column to read from
+ * @return a long value for the given field
+ */
+ public long getLong(int row, int col) {
+ acquireReference();
+ try {
+ return getLong_native(row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native long getLong_native(int row, int col);
+
+ /**
+ * Returns a double for the given field.
+ * row is 0 based
+ *
+ * @param row the row to read from, row - getStartPosition() being the actual row in the window
+ * @param col the column to read from
+ * @return a double value for the given field
+ */
+ public double getDouble(int row, int col) {
+ acquireReference();
+ try {
+ return getDouble_native(row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native double getDouble_native(int row, int col);
+
+ /**
+ * Returns a short for the given field.
+ * row is 0 based
+ *
+ * @param row the row to read from, row - getStartPosition() being the actual row in the window
+ * @param col the column to read from
+ * @return a short value for the given field
+ */
+ public short getShort(int row, int col) {
+ acquireReference();
+ try {
+ return (short) getLong_native(row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /**
+ * Returns an int for the given field.
+ *
+ * @param row the row to read from, row - getStartPosition() being the actual row in the window
+ * @param col the column to read from
+ * @return an int value for the given field
+ */
+ public int getInt(int row, int col) {
+ acquireReference();
+ try {
+ return (int) getLong_native(row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /**
+ * Returns a float for the given field.
+ * row is 0 based
+ *
+ * @param row the row to read from, row - getStartPosition() being the actual row in the window
+ * @param col the column to read from
+ * @return a float value for the given field
+ */
+ public float getFloat(int row, int col) {
+ acquireReference();
+ try {
+ return (float) getDouble_native(row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /**
+ * Clears out the existing contents of the window, making it safe to reuse
+ * for new data. Note that the number of columns in the window may NOT
+ * change across a call to clear().
+ */
+ public void clear() {
+ acquireReference();
+ try {
+ mStartPos = 0;
+ native_clear();
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /** Clears out the native side of things */
+ private native void native_clear();
+
+ /**
+ * Cleans up the native resources associated with the window.
+ */
+ public void close() {
+ releaseReference();
+ }
+
+ private native void close_native();
+
+ @Override
+ protected void finalize() {
+ // Just in case someone forgot to call close...
+ close_native();
+ }
+
+ public static final 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) {
+ dest.writeStrongBinder(native_getBinder());
+ dest.writeInt(mStartPos);
+ }
+
+ private CursorWindow(Parcel source) {
+ IBinder nativeBinder = source.readStrongBinder();
+ mStartPos = source.readInt();
+
+ native_init(nativeBinder);
+ }
+
+ /** Get the binder for the native side of the window */
+ private native IBinder native_getBinder();
+
+ /** Does the native side initialization for an empty window */
+ private native void native_init(boolean localOnly);
+
+ /** Does the native side initialization with an existing binder from another process */
+ private native void native_init(IBinder nativeBinder);
+
+ @Override
+ protected void onAllReferencesReleased() {
+ close_native();
+ }
+}
diff --git a/core/java/android/database/CursorWrapper.java b/core/java/android/database/CursorWrapper.java
new file mode 100644
index 0000000..f0aa7d7
--- /dev/null
+++ b/core/java/android/database/CursorWrapper.java
@@ -0,0 +1,305 @@
+/*
+ * 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.content.ContentResolver;
+import android.database.CharArrayBuffer;
+import android.net.Uri;
+import android.os.Bundle;
+
+import java.util.Map;
+
+/**
+ * Wrapper class for Cursor that delegates all calls to the actual cursor object
+ */
+
+public class CursorWrapper implements Cursor {
+
+ public CursorWrapper(Cursor cursor) {
+ mCursor = cursor;
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public void abortUpdates() {
+ mCursor.abortUpdates();
+ }
+
+ public void close() {
+ mCursor.close();
+ }
+
+ public boolean isClosed() {
+ return mCursor.isClosed();
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean commitUpdates() {
+ return mCursor.commitUpdates();
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean commitUpdates(
+ Map<? extends Long, ? extends Map<String, Object>> values) {
+ return mCursor.commitUpdates(values);
+ }
+
+ public int getCount() {
+ return mCursor.getCount();
+ }
+
+ public void deactivate() {
+ mCursor.deactivate();
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean deleteRow() {
+ return mCursor.deleteRow();
+ }
+
+ public boolean moveToFirst() {
+ return mCursor.moveToFirst();
+ }
+
+ public int getColumnCount() {
+ return mCursor.getColumnCount();
+ }
+
+ public int getColumnIndex(String columnName) {
+ return mCursor.getColumnIndex(columnName);
+ }
+
+ public int getColumnIndexOrThrow(String columnName)
+ throws IllegalArgumentException {
+ return mCursor.getColumnIndexOrThrow(columnName);
+ }
+
+ public String getColumnName(int columnIndex) {
+ return mCursor.getColumnName(columnIndex);
+ }
+
+ public String[] getColumnNames() {
+ return mCursor.getColumnNames();
+ }
+
+ public double getDouble(int columnIndex) {
+ return mCursor.getDouble(columnIndex);
+ }
+
+ public Bundle getExtras() {
+ return mCursor.getExtras();
+ }
+
+ public float getFloat(int columnIndex) {
+ return mCursor.getFloat(columnIndex);
+ }
+
+ public int getInt(int columnIndex) {
+ return mCursor.getInt(columnIndex);
+ }
+
+ public long getLong(int columnIndex) {
+ return mCursor.getLong(columnIndex);
+ }
+
+ public short getShort(int columnIndex) {
+ return mCursor.getShort(columnIndex);
+ }
+
+ public String getString(int columnIndex) {
+ return mCursor.getString(columnIndex);
+ }
+
+ public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
+ mCursor.copyStringToBuffer(columnIndex, buffer);
+ }
+
+ public byte[] getBlob(int columnIndex) {
+ return mCursor.getBlob(columnIndex);
+ }
+
+ public boolean getWantsAllOnMoveCalls() {
+ return mCursor.getWantsAllOnMoveCalls();
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean hasUpdates() {
+ return mCursor.hasUpdates();
+ }
+
+ public boolean isAfterLast() {
+ return mCursor.isAfterLast();
+ }
+
+ public boolean isBeforeFirst() {
+ return mCursor.isBeforeFirst();
+ }
+
+ public boolean isFirst() {
+ return mCursor.isFirst();
+ }
+
+ public boolean isLast() {
+ return mCursor.isLast();
+ }
+
+ public boolean isNull(int columnIndex) {
+ return mCursor.isNull(columnIndex);
+ }
+
+ public boolean moveToLast() {
+ return mCursor.moveToLast();
+ }
+
+ public boolean move(int offset) {
+ return mCursor.move(offset);
+ }
+
+ public boolean moveToPosition(int position) {
+ return mCursor.moveToPosition(position);
+ }
+
+ public boolean moveToNext() {
+ return mCursor.moveToNext();
+ }
+
+ public int getPosition() {
+ return mCursor.getPosition();
+ }
+
+ public boolean moveToPrevious() {
+ return mCursor.moveToPrevious();
+ }
+
+ public void registerContentObserver(ContentObserver observer) {
+ mCursor.registerContentObserver(observer);
+ }
+
+ public void registerDataSetObserver(DataSetObserver observer) {
+ mCursor.registerDataSetObserver(observer);
+ }
+
+ public boolean requery() {
+ return mCursor.requery();
+ }
+
+ public Bundle respond(Bundle extras) {
+ return mCursor.respond(extras);
+ }
+
+ public void setNotificationUri(ContentResolver cr, Uri uri) {
+ mCursor.setNotificationUri(cr, uri);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean supportsUpdates() {
+ return mCursor.supportsUpdates();
+ }
+
+ public void unregisterContentObserver(ContentObserver observer) {
+ mCursor.unregisterContentObserver(observer);
+ }
+
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ mCursor.unregisterDataSetObserver(observer);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateDouble(int columnIndex, double value) {
+ return mCursor.updateDouble(columnIndex, value);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateFloat(int columnIndex, float value) {
+ return mCursor.updateFloat(columnIndex, value);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateInt(int columnIndex, int value) {
+ return mCursor.updateInt(columnIndex, value);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateLong(int columnIndex, long value) {
+ return mCursor.updateLong(columnIndex, value);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateShort(int columnIndex, short value) {
+ return mCursor.updateShort(columnIndex, value);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateString(int columnIndex, String value) {
+ return mCursor.updateString(columnIndex, value);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateBlob(int columnIndex, byte[] value) {
+ return mCursor.updateBlob(columnIndex, value);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateToNull(int columnIndex) {
+ return mCursor.updateToNull(columnIndex);
+ }
+
+ private Cursor mCursor;
+
+}
+
diff --git a/core/java/android/database/DataSetObservable.java b/core/java/android/database/DataSetObservable.java
new file mode 100644
index 0000000..9200e81
--- /dev/null
+++ b/core/java/android/database/DataSetObservable.java
@@ -0,0 +1,47 @@
+/*
+ * 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 Observable for DataSetObserver that provides methods for
+ * invoking the various callback methods of DataSetObserver.
+ */
+public class DataSetObservable extends Observable<DataSetObserver> {
+ /**
+ * Invokes onChanged on each observer. Called when the data set being observed has
+ * changed, and which when read contains the new state of the data.
+ */
+ public void notifyChanged() {
+ synchronized(mObservers) {
+ for (DataSetObserver observer : mObservers) {
+ observer.onChanged();
+ }
+ }
+ }
+
+ /**
+ * Invokes onInvalidated on each observer. Called when the data set being monitored
+ * has changed such that it is no longer valid.
+ */
+ public void notifyInvalidated() {
+ synchronized (mObservers) {
+ for (DataSetObserver observer : mObservers) {
+ observer.onInvalidated();
+ }
+ }
+ }
+}
diff --git a/core/java/android/database/DataSetObserver.java b/core/java/android/database/DataSetObserver.java
new file mode 100644
index 0000000..28616c8
--- /dev/null
+++ b/core/java/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/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java
new file mode 100644
index 0000000..10f3806
--- /dev/null
+++ b/core/java/android/database/DatabaseUtils.java
@@ -0,0 +1,1018 @@
+/*
+ * 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 org.apache.commons.codec.binary.Hex;
+
+import android.content.ContentValues;
+import android.content.Context;
+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.Parcel;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+
+import java.io.FileNotFoundException;
+import java.io.PrintStream;
+import java.text.Collator;
+import java.util.HashMap;
+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;
+ private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV;
+
+ private static final String[] countProjection = new String[]{"count(*)"};
+
+ /**
+ * 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 {
+ 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.readInt();
+ if (code == 0) return;
+ String msg = reply.readString();
+ DatabaseUtils.readExceptionFromParcel(reply, msg, code);
+ }
+
+ public static void readExceptionWithFileNotFoundExceptionFromParcel(
+ Parcel reply) throws FileNotFoundException {
+ int code = reply.readInt();
+ if (code == 0) return;
+ String msg = reply.readString();
+ if (code == 1) {
+ throw new FileNotFoundException(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);
+ 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());
+ }
+ }
+
+ /**
+ * 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.
+ * @hide
+ */
+ 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 = Hex.encodeHex(arr);
+ return new String(keys, 0, getKeyLen(arr) * 2);
+ }
+
+ 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) {
+ AbstractWindowedCursor awc =
+ (cursor instanceof AbstractWindowedCursor) ? (AbstractWindowedCursor) cursor : null;
+
+ String[] columns = cursor.getColumnNames();
+ int length = columns.length;
+ for (int i = 0; i < length; i++) {
+ if (awc != null && awc.isBlob(i)) {
+ values.put(columns[i], cursor.getBlob(i));
+ } else {
+ values.put(columns[i], cursor.getString(i));
+ }
+ }
+ }
+
+ /**
+ * 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) {
+ Cursor cursor = db.query(table, countProjection,
+ null, null, null, null, null);
+ cursor.moveToFirst();
+ long count = cursor.getLong(0);
+ cursor.deactivate();
+ return count;
+ }
+
+ /**
+ * 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) {
+ if (selectionArgs != null) {
+ int size = selectionArgs.length;
+ for (int i = 0; i < size; i++) {
+ bindObjectToProgram(prog, i + 1, selectionArgs[i]);
+ }
+ }
+ long value = prog.simpleQueryForLong();
+ return value;
+ }
+
+ /**
+ * 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) {
+ if (selectionArgs != null) {
+ int size = selectionArgs.length;
+ for (int i = 0; i < size; i++) {
+ bindObjectToProgram(prog, i + 1, selectionArgs[i]);
+ }
+ }
+ String value = prog.simpleQueryForString();
+ return value;
+ }
+
+ /**
+ * This class allows users to do multiple inserts into a table but
+ * compile the SQL insert statement only once, which may increase
+ * performance.
+ */
+ 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;
+ 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 (LOCAL_LOGV) 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 synchronized long insertInternal(ContentValues values, boolean allowReplace) {
+ try {
+ SQLiteStatement stmt = getStatement(allowReplace);
+ stmt.clearBindings();
+ if (LOCAL_LOGV) 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 (LOCAL_LOGV) {
+ Log.v(TAG, "binding " + e.getValue() + " to column " +
+ i + " (" + key + ")");
+ }
+ }
+ return stmt.executeInsert();
+ } catch (SQLException e) {
+ Log.e(TAG, "Error inserting " + values + " into table " + mTableName, e);
+ return -1;
+ }
+ }
+
+ /**
+ * 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 (LOCAL_LOGV) 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();
+ }
+}
diff --git a/core/java/android/database/IBulkCursor.java b/core/java/android/database/IBulkCursor.java
new file mode 100644
index 0000000..24354fd
--- /dev/null
+++ b/core/java/android/database/IBulkCursor.java
@@ -0,0 +1,90 @@
+/*
+ * 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.RemoteException;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.Bundle;
+
+import java.util.Map;
+
+/**
+ * This interface provides a low-level way to pass bulk cursor data across
+ * both process and language boundries. Application code should use the Cursor
+ * interface directly.
+ *
+ * {@hide}
+ */
+public interface IBulkCursor extends IInterface
+{
+ /**
+ * Returns a BulkCursorWindow, which either has a reference to a shared
+ * memory segment with the rows, or an array of JSON strings.
+ */
+ public CursorWindow getWindow(int startPos) throws RemoteException;
+
+ public void onMove(int position) throws RemoteException;
+
+ /**
+ * Returns the number of rows in the cursor.
+ *
+ * @return the number of rows in the cursor.
+ */
+ public int count() throws RemoteException;
+
+ /**
+ * Returns a string array holding the names of all of the columns in the
+ * cursor in the order in which they were listed in the result.
+ *
+ * @return the names of the columns returned in this query.
+ */
+ public String[] getColumnNames() throws RemoteException;
+
+ public boolean updateRows(Map<? extends Long, ? extends Map<String, Object>> values) throws RemoteException;
+
+ public boolean deleteRow(int position) throws RemoteException;
+
+ public void deactivate() throws RemoteException;
+
+ public void close() throws RemoteException;
+
+ public int requery(IContentObserver observer, CursorWindow window) throws RemoteException;
+
+ boolean getWantsAllOnMoveCalls() 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 COUNT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 1;
+ static final int GET_COLUMN_NAMES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 2;
+ static final int UPDATE_ROWS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 3;
+ static final int DELETE_ROW_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 4;
+ static final int DEACTIVATE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 5;
+ static final int REQUERY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 6;
+ static final int ON_MOVE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 7;
+ static final int WANTS_ON_MOVE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 8;
+ static final int GET_EXTRAS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 9;
+ static final int RESPOND_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 10;
+ static final int CLOSE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 11;
+}
+
diff --git a/core/java/android/database/IContentObserver.aidl b/core/java/android/database/IContentObserver.aidl
new file mode 100755
index 0000000..ac2f975
--- /dev/null
+++ b/core/java/android/database/IContentObserver.aidl
@@ -0,0 +1,31 @@
+/*
+**
+** Copyright 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;
+
+/**
+ * @hide
+ */
+interface IContentObserver
+{
+ /**
+ * This method is called when an update occurs to the cursor that is being
+ * observed. selfUpdate is true if the update was caused by a call to
+ * commit on the cursor that is being observed.
+ */
+ oneway void onChange(boolean selfUpdate);
+}
diff --git a/core/java/android/database/MatrixCursor.java b/core/java/android/database/MatrixCursor.java
new file mode 100644
index 0000000..cf5a573
--- /dev/null
+++ b/core/java/android/database/MatrixCursor.java
@@ -0,0 +1,267 @@
+/*
+ * 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;
+
+/**
+ * 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;
+ private Object[] data;
+ 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.
+ */
+ 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() {
+ rowCount++;
+ int endIndex = rowCount * columnCount;
+ ensureCapacity(endIndex);
+ int start = endIndex - columnCount;
+ return new RowBuilder(start, endIndex);
+ }
+
+ /**
+ * 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, starting from the left-most column and adding one column
+ * value at a time. Follows the same ordering as the column names specified
+ * at cursor construction time.
+ */
+ public class RowBuilder {
+
+ private int index;
+ private final int endIndex;
+
+ RowBuilder(int index, int endIndex) {
+ this.index = index;
+ this.endIndex = endIndex;
+ }
+
+ /**
+ * 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;
+ }
+ }
+
+ // AbstractCursor implementation.
+
+ public int getCount() {
+ return rowCount;
+ }
+
+ public String[] getColumnNames() {
+ return columnNames;
+ }
+
+ public String getString(int column) {
+ return String.valueOf(get(column));
+ }
+
+ public short getShort(int column) {
+ Object value = get(column);
+ return (value instanceof String)
+ ? Short.valueOf((String) value)
+ : ((Number) value).shortValue();
+ }
+
+ public int getInt(int column) {
+ Object value = get(column);
+ return (value instanceof String)
+ ? Integer.valueOf((String) value)
+ : ((Number) value).intValue();
+ }
+
+ public long getLong(int column) {
+ Object value = get(column);
+ return (value instanceof String)
+ ? Long.valueOf((String) value)
+ : ((Number) value).longValue();
+ }
+
+ public float getFloat(int column) {
+ Object value = get(column);
+ return (value instanceof String)
+ ? Float.valueOf((String) value)
+ : ((Number) value).floatValue();
+ }
+
+ public double getDouble(int column) {
+ Object value = get(column);
+ return (value instanceof String)
+ ? Double.valueOf((String) value)
+ : ((Number) value).doubleValue();
+ }
+
+ public boolean isNull(int column) {
+ return get(column) == null;
+ }
+}
diff --git a/core/java/android/database/MergeCursor.java b/core/java/android/database/MergeCursor.java
new file mode 100644
index 0000000..7e91159
--- /dev/null
+++ b/core/java/android/database/MergeCursor.java
@@ -0,0 +1,257 @@
+/*
+ * 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 convience 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;
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ @Override
+ public boolean deleteRow()
+ {
+ return mCursor.deleteRow();
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ @Override
+ public boolean commitUpdates() {
+ int length = mCursors.length;
+ for (int i = 0 ; i < length ; i++) {
+ if (mCursors[i] != null) {
+ mCursors[i].commitUpdates();
+ }
+ }
+ onChange(true);
+ return true;
+ }
+
+ @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 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();
+ }
+ }
+ }
+
+ @Override
+ public void close() {
+ int length = mCursors.length;
+ for (int i = 0 ; i < length ; i++) {
+ if (mCursors[i] == null) continue;
+ mCursors[i].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/core/java/android/database/Observable.java b/core/java/android/database/Observable.java
new file mode 100644
index 0000000..b6fecab
--- /dev/null
+++ b/core/java/android/database/Observable.java
@@ -0,0 +1,78 @@
+/*
+ * 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 (un)registering arbitrary observers in an ArrayList.
+ */
+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 observer
+ */
+ public void unregisterAll() {
+ synchronized(mObservers) {
+ mObservers.clear();
+ }
+ }
+}
diff --git a/core/java/android/database/SQLException.java b/core/java/android/database/SQLException.java
new file mode 100644
index 0000000..0386af0
--- /dev/null
+++ b/core/java/android/database/SQLException.java
@@ -0,0 +1,30 @@
+/*
+ * 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);
+ }
+}
diff --git a/core/java/android/database/StaleDataException.java b/core/java/android/database/StaleDataException.java
new file mode 100644
index 0000000..ee70beb
--- /dev/null
+++ b/core/java/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/core/java/android/database/package.html b/core/java/android/database/package.html
new file mode 100644
index 0000000..1f76d9f
--- /dev/null
+++ b/core/java/android/database/package.html
@@ -0,0 +1,14 @@
+<HTML>
+<BODY>
+Contains classes to explore data returned through a content provider.
+<p>
+If you need to manage data in a private database, use the {@link
+android.database.sqlite} classes. These classes are used to manage the {@link
+android.database.Cursor} object returned from a content provider query. Databases
+are usually created and opened with {@link android.content.Context#openOrCreateDatabase}
+To make requests through
+content providers, you can use the {@link android.content.ContentResolver
+content.ContentResolver} class.
+<p>All databases are stored on the device in <code>/data/data/&lt;package_name&gt;/databases</code>
+</BODY>
+</HTML>
diff --git a/core/java/android/database/sqlite/SQLiteAbortException.java b/core/java/android/database/sqlite/SQLiteAbortException.java
new file mode 100644
index 0000000..64dc4b7
--- /dev/null
+++ b/core/java/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/core/java/android/database/sqlite/SQLiteClosable.java b/core/java/android/database/sqlite/SQLiteClosable.java
new file mode 100644
index 0000000..f64261c
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteClosable.java
@@ -0,0 +1,55 @@
+/*
+ * 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;
+
+/**
+ * An object create from a SQLiteDatabase that can be closed.
+ */
+public abstract class SQLiteClosable {
+ private int mReferenceCount = 1;
+ private Object mLock = new Object();
+ protected abstract void onAllReferencesReleased();
+ protected void onAllReferencesReleasedFromContainer(){}
+
+ public void acquireReference() {
+ synchronized(mLock) {
+ if (mReferenceCount <= 0) {
+ throw new IllegalStateException(
+ "attempt to acquire a reference on a close SQLiteClosable");
+ }
+ mReferenceCount++;
+ }
+ }
+
+ public void releaseReference() {
+ synchronized(mLock) {
+ mReferenceCount--;
+ if (mReferenceCount == 0) {
+ onAllReferencesReleased();
+ }
+ }
+ }
+
+ public void releaseReferenceFromContainer() {
+ synchronized(mLock) {
+ mReferenceCount--;
+ if (mReferenceCount == 0) {
+ onAllReferencesReleasedFromContainer();
+ }
+ }
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteConstraintException.java b/core/java/android/database/sqlite/SQLiteConstraintException.java
new file mode 100644
index 0000000..e3119eb
--- /dev/null
+++ b/core/java/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/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java
new file mode 100644
index 0000000..70b9b83
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteCursor.java
@@ -0,0 +1,606 @@
+/*
+ * 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.AbstractWindowedCursor;
+import android.database.CursorWindow;
+import android.database.DataSetObserver;
+import android.database.SQLException;
+
+import android.os.Handler;
+import android.os.Message;
+import android.os.Process;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * A Cursor implementation that exposes results from a query on a
+ * {@link SQLiteDatabase}.
+ */
+public class SQLiteCursor extends AbstractWindowedCursor {
+ static final String TAG = "Cursor";
+ static final int NO_COUNT = -1;
+
+ /** The name of the table to edit */
+ private String mEditTable;
+
+ /** The names of the columns in the rows */
+ private String[] mColumns;
+
+ /** The query object for the cursor */
+ private SQLiteQuery mQuery;
+
+ /** The database the cursor was created from */
+ private SQLiteDatabase mDatabase;
+
+ /** The compiled query this cursor came from */
+ private SQLiteCursorDriver mDriver;
+
+ /** The number of rows in the cursor */
+ private int mCount = NO_COUNT;
+
+ /** 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 StackTraceElement[] mStackTraceElements;
+
+ /**
+ * mMaxRead is the max items that each cursor window reads
+ * default to a very high value
+ */
+ private int mMaxRead = Integer.MAX_VALUE;
+ private int mInitialRead = Integer.MAX_VALUE;
+ private int mCursorState = 0;
+ private ReentrantLock mLock = null;
+ private boolean mPendingData = false;
+
+ /**
+ * support for a cursor variant that doesn't always read all results
+ * initialRead is the initial number of items that cursor window reads
+ * if query contains more than this number of items, a thread will be
+ * created and handle the left over items so that caller can show
+ * results as soon as possible
+ * @param initialRead initial number of items that cursor read
+ * @param maxRead leftover items read at maxRead items per time
+ * @hide
+ */
+ public void setLoadStyle(int initialRead, int maxRead) {
+ mMaxRead = maxRead;
+ mInitialRead = initialRead;
+ mLock = new ReentrantLock(true);
+ }
+
+ private void queryThreadLock() {
+ if (mLock != null) {
+ mLock.lock();
+ }
+ }
+
+ private void queryThreadUnlock() {
+ if (mLock != null) {
+ mLock.unlock();
+ }
+ }
+
+
+ /**
+ * @hide
+ */
+ final private class QueryThread implements Runnable {
+ private final int mThreadState;
+ QueryThread(int version) {
+ mThreadState = version;
+ }
+ private void sendMessage() {
+ if (mNotificationHandler != null) {
+ mNotificationHandler.sendEmptyMessage(1);
+ mPendingData = false;
+ } else {
+ mPendingData = true;
+ }
+
+ }
+ public void run() {
+ // use cached mWindow, to avoid get null mWindow
+ CursorWindow cw = mWindow;
+ Process.setThreadPriority(Process.myTid(), Process.THREAD_PRIORITY_BACKGROUND);
+ // the cursor's state doesn't change
+ while (true) {
+ mLock.lock();
+ if (mCursorState != mThreadState) {
+ mLock.unlock();
+ break;
+ }
+ try {
+ int count = mQuery.fillWindow(cw, mMaxRead, mCount);
+ // return -1 means not finished
+ if (count != 0) {
+ if (count == NO_COUNT){
+ mCount += mMaxRead;
+ sendMessage();
+ } else {
+ mCount = count;
+ sendMessage();
+ break;
+ }
+ } else {
+ break;
+ }
+ } catch (Exception e) {
+ // end the tread when the cursor is close
+ break;
+ } finally {
+ mLock.unlock();
+ }
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ protected class MainThreadNotificationHandler extends Handler {
+ public void handleMessage(Message msg) {
+ notifyDataSetChange();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ protected MainThreadNotificationHandler mNotificationHandler;
+
+ public void registerDataSetObserver(DataSetObserver observer) {
+ super.registerDataSetObserver(observer);
+ if ((Integer.MAX_VALUE != mMaxRead || Integer.MAX_VALUE != mInitialRead) &&
+ mNotificationHandler == null) {
+ queryThreadLock();
+ try {
+ mNotificationHandler = new MainThreadNotificationHandler();
+ if (mPendingData) {
+ notifyDataSetChange();
+ mPendingData = false;
+ }
+ } finally {
+ queryThreadUnlock();
+ }
+ }
+
+ }
+
+ /**
+ * 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. This constructor
+ * has package scope.
+ *
+ * @param db a reference to a Database object that is already constructed
+ * and opened
+ * @param editTable the name of the table used for this query
+ * @param query the rest of the query terms
+ * cursor is finalized
+ */
+ public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver,
+ String editTable, SQLiteQuery query) {
+ // The AbstractCursor constructor needs to do some setup.
+ super();
+
+ if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) {
+ mStackTraceElements = new Exception().getStackTrace();
+ }
+
+ mDatabase = db;
+ mDriver = driver;
+ mEditTable = editTable;
+ mColumnNameMap = null;
+ mQuery = query;
+
+ try {
+ db.lock();
+
+ // Setup the list of columns
+ int columnCount = mQuery.columnCountLocked();
+ mColumns = new String[columnCount];
+
+ // Read in all column names
+ for (int i = 0; i < columnCount; i++) {
+ String columnName = mQuery.columnNameLocked(i);
+ mColumns[i] = columnName;
+ if (Config.LOGV) {
+ Log.v("DatabaseWindow", "mColumns[" + i + "] is "
+ + mColumns[i]);
+ }
+
+ // Make note of the row ID column index for quick access to it
+ if ("_id".equals(columnName)) {
+ mRowIdColumnIndex = i;
+ }
+ }
+ } finally {
+ db.unlock();
+ }
+ }
+
+ /**
+ * @return the SQLiteDatabase that this cursor is associated with.
+ */
+ public SQLiteDatabase getDatabase() {
+ return mDatabase;
+ }
+
+ @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;
+ }
+
+ private void fillWindow (int startPos) {
+ if (mWindow == null) {
+ // If there isn't a window set already it will only be accessed locally
+ mWindow = new CursorWindow(true /* the window is local only */);
+ } else {
+ mCursorState++;
+ queryThreadLock();
+ try {
+ mWindow.clear();
+ } finally {
+ queryThreadUnlock();
+ }
+ }
+ mWindow.setStartPosition(startPos);
+ mCount = mQuery.fillWindow(mWindow, mInitialRead, 0);
+ // return -1 means not finished
+ if (mCount == NO_COUNT){
+ mCount = startPos + mInitialRead;
+ Thread t = new Thread(new QueryThread(mCursorState), "query thread");
+ t.start();
+ }
+ }
+
+ @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;
+ }
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ @Override
+ public boolean deleteRow() {
+ checkPosition();
+
+ // Only allow deletes if there is an ID column, and the ID has been read from it
+ if (mRowIdColumnIndex == -1 || mCurrentRowID == null) {
+ Log.e(TAG,
+ "Could not delete row because either the row ID column is not available or it" +
+ "has not been read.");
+ return false;
+ }
+
+ boolean success;
+
+ /*
+ * Ensure we don't change the state of the database when another
+ * thread is holding the database lock. requery() and moveTo() are also
+ * synchronized here to make sure they get the state of the database
+ * immediately following the DELETE.
+ */
+ mDatabase.lock();
+ try {
+ try {
+ mDatabase.delete(mEditTable, mColumns[mRowIdColumnIndex] + "=?",
+ new String[] {mCurrentRowID.toString()});
+ success = true;
+ } catch (SQLException e) {
+ success = false;
+ }
+
+ int pos = mPos;
+ requery();
+
+ /*
+ * Ensure proper cursor state. Note that mCurrentRowID changes
+ * in this call.
+ */
+ moveToPosition(pos);
+ } finally {
+ mDatabase.unlock();
+ }
+
+ if (success) {
+ onChange(true);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ return mColumns;
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ @Override
+ public boolean supportsUpdates() {
+ return super.supportsUpdates() && !TextUtils.isEmpty(mEditTable);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ @Override
+ public boolean commitUpdates(Map<? extends Long,
+ ? extends Map<String, Object>> additionalValues) {
+ if (!supportsUpdates()) {
+ Log.e(TAG, "commitUpdates not supported on this cursor, did you "
+ + "include the _id column?");
+ return false;
+ }
+
+ /*
+ * Prevent other threads from changing the updated rows while they're
+ * being processed here.
+ */
+ synchronized (mUpdatedRows) {
+ if (additionalValues != null) {
+ mUpdatedRows.putAll(additionalValues);
+ }
+
+ if (mUpdatedRows.size() == 0) {
+ return true;
+ }
+
+ /*
+ * Prevent other threads from changing the database state while
+ * we process the updated rows, and prevents us from changing the
+ * database behind the back of another thread.
+ */
+ mDatabase.beginTransaction();
+ try {
+ StringBuilder sql = new StringBuilder(128);
+
+ // For each row that has been updated
+ for (Map.Entry<Long, Map<String, Object>> rowEntry :
+ mUpdatedRows.entrySet()) {
+ Map<String, Object> values = rowEntry.getValue();
+ Long rowIdObj = rowEntry.getKey();
+
+ if (rowIdObj == null || values == null) {
+ throw new IllegalStateException("null rowId or values found! rowId = "
+ + rowIdObj + ", values = " + values);
+ }
+
+ if (values.size() == 0) {
+ continue;
+ }
+
+ long rowId = rowIdObj.longValue();
+
+ Iterator<Map.Entry<String, Object>> valuesIter =
+ values.entrySet().iterator();
+
+ sql.setLength(0);
+ sql.append("UPDATE " + mEditTable + " SET ");
+
+ // For each column value that has been updated
+ Object[] bindings = new Object[values.size()];
+ int i = 0;
+ while (valuesIter.hasNext()) {
+ Map.Entry<String, Object> entry = valuesIter.next();
+ sql.append(entry.getKey());
+ sql.append("=?");
+ bindings[i] = entry.getValue();
+ if (valuesIter.hasNext()) {
+ sql.append(", ");
+ }
+ i++;
+ }
+
+ sql.append(" WHERE " + mColumns[mRowIdColumnIndex]
+ + '=' + rowId);
+ sql.append(';');
+ mDatabase.execSQL(sql.toString(), bindings);
+ mDatabase.rowUpdated(mEditTable, rowId);
+ }
+ mDatabase.setTransactionSuccessful();
+ } finally {
+ mDatabase.endTransaction();
+ }
+
+ mUpdatedRows.clear();
+ }
+
+ // Let any change observers know about the update
+ onChange(true);
+
+ return true;
+ }
+
+ private void deactivateCommon() {
+ if (Config.LOGV) Log.v(TAG, "<<< Releasing cursor " + this);
+ mCursorState = 0;
+ if (mWindow != null) {
+ mWindow.close();
+ mWindow = null;
+ }
+ if (Config.LOGV) Log.v("DatabaseWindow", "closing window in release()");
+ }
+
+ @Override
+ public void deactivate() {
+ super.deactivate();
+ deactivateCommon();
+ mDriver.cursorDeactivated();
+ }
+
+ @Override
+ public void close() {
+ super.close();
+ deactivateCommon();
+ mQuery.close();
+ mDriver.cursorClosed();
+ }
+
+ @Override
+ public boolean requery() {
+ if (isClosed()) {
+ return false;
+ }
+ long timeStart = 0;
+ if (Config.LOGV) {
+ timeStart = System.currentTimeMillis();
+ }
+ /*
+ * Synchronize on the database lock to ensure that mCount matches the
+ * results of mQuery.requery().
+ */
+ mDatabase.lock();
+ try {
+ if (mWindow != null) {
+ mWindow.clear();
+ }
+ mPos = -1;
+ // This one will recreate the temp table, and get its count
+ mDriver.cursorRequeried(this);
+ mCount = NO_COUNT;
+ mCursorState++;
+ queryThreadLock();
+ try {
+ mQuery.requery();
+ } finally {
+ queryThreadUnlock();
+ }
+ } finally {
+ mDatabase.unlock();
+ }
+
+ if (Config.LOGV) {
+ Log.v("DatabaseWindow", "closing window in requery()");
+ Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery);
+ }
+
+ boolean result = super.requery();
+ if (Config.LOGV) {
+ long timeEnd = System.currentTimeMillis();
+ Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString());
+ }
+ return result;
+ }
+
+ @Override
+ public void setWindow(CursorWindow window) {
+ if (mWindow != null) {
+ mCursorState++;
+ queryThreadLock();
+ try {
+ mWindow.close();
+ } finally {
+ queryThreadUnlock();
+ }
+ mCount = NO_COUNT;
+ }
+ mWindow = window;
+ }
+
+ /**
+ * Changes the selection arguments. The new values take effect after a call to requery().
+ */
+ public void setSelectionArguments(String[] selectionArgs) {
+ mDriver.setBindArguments(selectionArgs);
+ }
+
+ /**
+ * Release the native resources, if they haven't been released yet.
+ */
+ @Override
+ protected void finalize() {
+ try {
+ if (mWindow != null) {
+ close();
+ String message = "Finalizing cursor " + this + " on " + mEditTable
+ + " that has not been deactivated or closed";
+ if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) {
+ Log.d(TAG, message + "\nThis cursor was created in:");
+ for (StackTraceElement ste : mStackTraceElements) {
+ Log.d(TAG, " " + ste);
+ }
+ }
+ SQLiteDebug.notifyActiveCursorFinalized();
+ throw new IllegalStateException(message);
+ } else {
+ if (Config.LOGV) {
+ Log.v(TAG, "Finalizing cursor " + this + " on " + mEditTable);
+ }
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteCursorDriver.java b/core/java/android/database/sqlite/SQLiteCursorDriver.java
new file mode 100644
index 0000000..eda1b78
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteCursorDriver.java
@@ -0,0 +1,58 @@
+/*
+ * 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 requeryed.
+ *
+ * @return The new count value.
+ */
+ 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/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java
new file mode 100644
index 0000000..2af080a
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -0,0 +1,1675 @@
+/*
+ * 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.content.ContentValues;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.SQLException;
+import android.os.Debug;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+import android.util.EventLog;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Exposes methods to manage a SQLite database.
+ * <p>SQLiteDatabase has methods to create, delete, execute SQL commands, and
+ * perform other common database management tasks.
+ * <p>See the Notepad sample application in the SDK for an example of creating
+ * and managing a database.
+ * <p> Database names must be unique within an application, not across all
+ * applications.
+ *
+ * <h3>Localized Collation - ORDER BY</h3>
+ * <p>In addition to SQLite's default <code>BINARY</code> collator, Android supplies
+ * two more, <code>LOCALIZED</code>, which changes with the system's current locale
+ * if you wire it up correctly (XXX a link needed!), and <code>UNICODE</code>, which
+ * is the Unicode Collation Algorithm and not tailored to the current locale.
+ */
+public class SQLiteDatabase extends SQLiteClosable {
+ private static final String TAG = "Database";
+ private static final int DB_OPERATION_EVENT = 52000;
+
+ /**
+ * Algorithms used in ON CONFLICT clause
+ * http://www.sqlite.org/lang_conflict.html
+ * @hide
+ */
+ public enum ConflictAlgorithm {
+ /**
+ * 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.
+ */
+ ROLLBACK("ROLLBACK"),
+
+ /**
+ * 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.
+ */
+ ABORT("ABORT"),
+
+ /**
+ * 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.
+ */
+ FAIL("FAIL"),
+
+ /**
+ * 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.
+ */
+ IGNORE("IGNORE"),
+
+ /**
+ * 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.
+ */
+ REPLACE("REPLACE");
+
+ private final String mValue;
+ ConflictAlgorithm(String value) {
+ mValue = value;
+ }
+ public String value() {
+ return mValue;
+ }
+ }
+
+ /**
+ * Maximum Length Of A LIKE Or GLOB Pattern
+ * The pattern matching algorithm used in the default LIKE and GLOB implementation
+ * of SQLite can exhibit O(N^2) performance (where N is the number of characters in
+ * the pattern) for certain pathological cases. To avoid denial-of-service attacks
+ * the length of the LIKE or GLOB pattern is limited to SQLITE_MAX_LIKE_PATTERN_LENGTH bytes.
+ * The default value of this limit is 50000. A modern workstation can evaluate
+ * even a pathological LIKE or GLOB pattern of 50000 bytes relatively quickly.
+ * The denial of service problem only comes into play when the pattern length gets
+ * into millions of bytes. Nevertheless, since most useful LIKE or GLOB patterns
+ * are at most a few dozen bytes in length, paranoid application developers may
+ * want to reduce this parameter to something in the range of a few hundred
+ * if they know that external users are able to generate arbitrary patterns.
+ */
+ public static final int SQLITE_MAX_LIKE_PATTERN_LENGTH = 50000;
+
+ /**
+ * Flag for {@link #openDatabase} to open the database for reading and writing.
+ * If the disk is full, this may fail even before you actually write anything.
+ *
+ * {@more} Note that the value of this flag is 0, so it is the default.
+ */
+ public static final int OPEN_READWRITE = 0x00000000; // update native code if changing
+
+ /**
+ * Flag for {@link #openDatabase} to open the database for reading only.
+ * This is the only reliable way to open a database if the disk may be full.
+ */
+ public static final int OPEN_READONLY = 0x00000001; // update native code if changing
+
+ private static final int OPEN_READ_MASK = 0x00000001; // update native code if changing
+
+ /**
+ * Flag for {@link #openDatabase} to open the database without support for localized collators.
+ *
+ * {@more} This causes the collator <code>LOCALIZED</code> not to be created.
+ * You must be consistent when using this flag to use the setting the database was
+ * created with. If this is set, {@link #setLocale} will do nothing.
+ */
+ public static final int NO_LOCALIZED_COLLATORS = 0x00000010; // update native code if changing
+
+ /**
+ * Flag for {@link #openDatabase} to create the database file if it does not already exist.
+ */
+ public static final int CREATE_IF_NECESSARY = 0x10000000; // update native code if changing
+
+ /**
+ * Indicates whether the most-recently started transaction has been marked as successful.
+ */
+ private boolean mInnerTransactionIsSuccessful;
+
+ /**
+ * Valid during the life of a transaction, and indicates whether the entire transaction (the
+ * outer one and all of the inner ones) so far has been successful.
+ */
+ private boolean mTransactionIsSuccessful;
+
+ /** Synchronize on this when accessing the database */
+ private final ReentrantLock mLock = new ReentrantLock(true);
+
+ private long mLockAcquiredWallTime = 0L;
+ private long mLockAcquiredThreadTime = 0L;
+
+ // limit the frequency of complaints about each database to one within 20 sec
+ // unless run command adb shell setprop log.tag.Database VERBOSE
+ private static final int LOCK_WARNING_WINDOW_IN_MS = 20000;
+ /** If the lock is held this long then a warning will be printed when it is released. */
+ private static final int LOCK_ACQUIRED_WARNING_TIME_IN_MS = 300;
+ private static final int LOCK_ACQUIRED_WARNING_THREAD_TIME_IN_MS = 100;
+ private static final int LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT = 2000;
+
+ private long mLastLockMessageTime = 0L;
+
+ /** Used by native code, do not rename */
+ /* package */ int mNativeHandle = 0;
+
+ /** Used to make temp table names unique */
+ /* package */ int mTempTableSequence = 0;
+
+ /** The path for the database file */
+ private String mPath;
+
+ /** The flags passed to open/create */
+ private int mFlags;
+
+ /** The optional factory to use when creating new Cursors */
+ private CursorFactory mFactory;
+
+ private WeakHashMap<SQLiteClosable, Object> mPrograms;
+
+ private final RuntimeException mLeakedException;
+
+ // package visible, since callers will access directly to minimize overhead in the case
+ // that logging is not enabled.
+ /* package */ final boolean mLogStats;
+
+ /**
+ * @param closable
+ */
+ void addSQLiteClosable(SQLiteClosable closable) {
+ lock();
+ try {
+ mPrograms.put(closable, null);
+ } finally {
+ unlock();
+ }
+ }
+
+ void removeSQLiteClosable(SQLiteClosable closable) {
+ lock();
+ try {
+ mPrograms.remove(closable);
+ } finally {
+ unlock();
+ }
+ }
+
+ @Override
+ protected void onAllReferencesReleased() {
+ if (isOpen()) {
+ dbclose();
+ }
+ }
+
+ /**
+ * Attempts to release memory that SQLite holds but does not require to
+ * operate properly. Typically this memory will come from the page cache.
+ *
+ * @return the number of bytes actually released
+ */
+ static public native int releaseMemory();
+
+ /**
+ * Control whether or not the SQLiteDatabase is made thread-safe by using locks
+ * around critical sections. This is pretty expensive, so if you know that your
+ * DB will only be used by a single thread then you should set this to false.
+ * The default is true.
+ * @param lockingEnabled set to true to enable locks, false otherwise
+ */
+ public void setLockingEnabled(boolean lockingEnabled) {
+ mLockingEnabled = lockingEnabled;
+ }
+
+ /**
+ * If set then the SQLiteDatabase is made thread-safe by using locks
+ * around critical sections
+ */
+ private boolean mLockingEnabled = true;
+
+ /* package */ void onCorruption() {
+ try {
+ // Close the database (if we can), which will cause subsequent operations to fail.
+ close();
+ } finally {
+ Log.e(TAG, "Removing corrupt database: " + mPath);
+ // Delete the corrupt file. Don't re-create it now -- that would just confuse people
+ // -- but the next time someone tries to open it, they can set it up from scratch.
+ new File(mPath).delete();
+ }
+ }
+
+ /**
+ * Locks the database for exclusive access. The database lock must be held when
+ * touch the native sqlite3* object since it is single threaded and uses
+ * a polling lock contention algorithm. The lock is recursive, and may be acquired
+ * multiple times by the same thread. This is a no-op if mLockingEnabled is false.
+ *
+ * @see #unlock()
+ */
+ /* package */ void lock() {
+ if (!mLockingEnabled) return;
+ mLock.lock();
+ if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) {
+ if (mLock.getHoldCount() == 1) {
+ // Use elapsed real-time since the CPU may sleep when waiting for IO
+ mLockAcquiredWallTime = SystemClock.elapsedRealtime();
+ mLockAcquiredThreadTime = Debug.threadCpuTimeNanos();
+ }
+ }
+ }
+
+ /**
+ * Locks the database for exclusive access. The database lock must be held when
+ * touch the native sqlite3* object since it is single threaded and uses
+ * a polling lock contention algorithm. The lock is recursive, and may be acquired
+ * multiple times by the same thread.
+ *
+ * @see #unlockForced()
+ */
+ private void lockForced() {
+ mLock.lock();
+ if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) {
+ if (mLock.getHoldCount() == 1) {
+ // Use elapsed real-time since the CPU may sleep when waiting for IO
+ mLockAcquiredWallTime = SystemClock.elapsedRealtime();
+ mLockAcquiredThreadTime = Debug.threadCpuTimeNanos();
+ }
+ }
+ }
+
+ /**
+ * Releases the database lock. This is a no-op if mLockingEnabled is false.
+ *
+ * @see #unlock()
+ */
+ /* package */ void unlock() {
+ if (!mLockingEnabled) return;
+ if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) {
+ if (mLock.getHoldCount() == 1) {
+ checkLockHoldTime();
+ }
+ }
+ mLock.unlock();
+ }
+
+ /**
+ * Releases the database lock.
+ *
+ * @see #unlockForced()
+ */
+ private void unlockForced() {
+ if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) {
+ if (mLock.getHoldCount() == 1) {
+ checkLockHoldTime();
+ }
+ }
+ mLock.unlock();
+ }
+
+ private void checkLockHoldTime() {
+ // Use elapsed real-time since the CPU may sleep when waiting for IO
+ long elapsedTime = SystemClock.elapsedRealtime();
+ long lockedTime = elapsedTime - mLockAcquiredWallTime;
+ if (lockedTime < LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT &&
+ !Log.isLoggable(TAG, Log.VERBOSE) &&
+ (elapsedTime - mLastLockMessageTime) < LOCK_WARNING_WINDOW_IN_MS) {
+ return;
+ }
+ if (lockedTime > LOCK_ACQUIRED_WARNING_TIME_IN_MS) {
+ int threadTime = (int)
+ ((Debug.threadCpuTimeNanos() - mLockAcquiredThreadTime) / 1000000);
+ if (threadTime > LOCK_ACQUIRED_WARNING_THREAD_TIME_IN_MS ||
+ lockedTime > LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT) {
+ mLastLockMessageTime = elapsedTime;
+ String msg = "lock held on " + mPath + " for " + lockedTime + "ms. Thread time was "
+ + threadTime + "ms";
+ if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING_STACK_TRACE) {
+ Log.d(TAG, msg, new Exception());
+ } else {
+ Log.d(TAG, msg);
+ }
+ }
+ }
+ }
+
+ /**
+ * Begins a transaction. Transactions can be nested. When the outer transaction is ended all of
+ * the work done in that transaction and all of the nested transactions will be committed or
+ * rolled back. The changes will be rolled back if any transaction is ended without being
+ * marked as clean (by calling setTransactionSuccessful). Otherwise they will be committed.
+ *
+ * <p>Here is the standard idiom for transactions:
+ *
+ * <pre>
+ * db.beginTransaction();
+ * try {
+ * ...
+ * db.setTransactionSuccessful();
+ * } finally {
+ * db.endTransaction();
+ * }
+ * </pre>
+ */
+ public void beginTransaction() {
+ lockForced();
+ boolean ok = false;
+ try {
+ // If this thread already had the lock then get out
+ if (mLock.getHoldCount() > 1) {
+ if (mInnerTransactionIsSuccessful) {
+ String msg = "Cannot call beginTransaction between "
+ + "calling setTransactionSuccessful and endTransaction";
+ IllegalStateException e = new IllegalStateException(msg);
+ Log.e(TAG, "beginTransaction() failed", e);
+ throw e;
+ }
+ ok = true;
+ return;
+ }
+
+ // This thread didn't already have the lock, so begin a database
+ // transaction now.
+ execSQL("BEGIN EXCLUSIVE;");
+ mTransactionIsSuccessful = true;
+ mInnerTransactionIsSuccessful = false;
+ ok = true;
+ } finally {
+ if (!ok) {
+ // beginTransaction is called before the try block so we must release the lock in
+ // the case of failure.
+ unlockForced();
+ }
+ }
+ }
+
+ /**
+ * End a transaction. See beginTransaction for notes about how to use this and when transactions
+ * are committed and rolled back.
+ */
+ public void endTransaction() {
+ if (!mLock.isHeldByCurrentThread()) {
+ throw new IllegalStateException("no transaction pending");
+ }
+ try {
+ if (mInnerTransactionIsSuccessful) {
+ mInnerTransactionIsSuccessful = false;
+ } else {
+ mTransactionIsSuccessful = false;
+ }
+ if (mLock.getHoldCount() != 1) {
+ return;
+ }
+ if (mTransactionIsSuccessful) {
+ execSQL("COMMIT;");
+ } else {
+ try {
+ execSQL("ROLLBACK;");
+ } catch (SQLException e) {
+ if (Config.LOGD) {
+ Log.d(TAG, "exception during rollback, maybe the DB previously "
+ + "performed an auto-rollback");
+ }
+ }
+ }
+ } finally {
+ unlockForced();
+ if (Config.LOGV) {
+ Log.v(TAG, "unlocked " + Thread.currentThread()
+ + ", holdCount is " + mLock.getHoldCount());
+ }
+ }
+ }
+
+ /**
+ * Marks the current transaction as successful. Do not do any more database work between
+ * calling this and calling endTransaction. Do as little non-database work as possible in that
+ * situation too. If any errors are encountered between this and endTransaction the transaction
+ * will still be committed.
+ *
+ * @throws IllegalStateException if the current thread is not in a transaction or the
+ * transaction is already marked as successful.
+ */
+ public void setTransactionSuccessful() {
+ if (!mLock.isHeldByCurrentThread()) {
+ throw new IllegalStateException("no transaction pending");
+ }
+ if (mInnerTransactionIsSuccessful) {
+ throw new IllegalStateException(
+ "setTransactionSuccessful may only be called once per call to beginTransaction");
+ }
+ mInnerTransactionIsSuccessful = true;
+ }
+
+ /**
+ * return true if there is a transaction pending
+ */
+ public boolean inTransaction() {
+ return mLock.getHoldCount() > 0;
+ }
+
+ /**
+ * Checks if the database lock is held by this thread.
+ *
+ * @return true, if this thread is holding the database lock.
+ */
+ public boolean isDbLockedByCurrentThread() {
+ return mLock.isHeldByCurrentThread();
+ }
+
+ /**
+ * Checks if the database is locked by another thread. This is
+ * just an estimate, since this status can change at any time,
+ * including after the call is made but before the result has
+ * been acted upon.
+ *
+ * @return true, if the database is locked by another thread
+ */
+ public boolean isDbLockedByOtherThreads() {
+ return !mLock.isHeldByCurrentThread() && mLock.isLocked();
+ }
+
+ /**
+ * Temporarily end the transaction to let other threads run. The transaction is assumed to be
+ * successful so far. Do not call setTransactionSuccessful before calling this. When this
+ * returns a new transaction will have been created but not marked as successful.
+ * @return true if the transaction was yielded
+ * @deprecated if the db is locked more than once (becuase of nested transactions) then the lock
+ * will not be yielded. Use yieldIfContendedSafely instead.
+ */
+ public boolean yieldIfContended() {
+ return yieldIfContendedHelper(false /* do not check yielding */);
+ }
+
+ /**
+ * 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
+ * through an exception if that is not the case.
+ * @return true if the transaction was yielded
+ */
+ public boolean yieldIfContendedSafely() {
+ return yieldIfContendedHelper(true /* check yielding */);
+ }
+
+ private boolean yieldIfContendedHelper(boolean checkFullyYielded) {
+ if (mLock.getQueueLength() == 0) {
+ // Reset the lock acquire time since we know that the thread was willing to yield
+ // the lock at this time.
+ mLockAcquiredWallTime = SystemClock.elapsedRealtime();
+ mLockAcquiredThreadTime = Debug.threadCpuTimeNanos();
+ return false;
+ }
+ setTransactionSuccessful();
+ endTransaction();
+ if (checkFullyYielded) {
+ if (this.isDbLockedByCurrentThread()) {
+ throw new IllegalStateException(
+ "Db locked more than once. yielfIfContended cannot yield");
+ }
+ }
+ beginTransaction();
+ return true;
+ }
+
+ /** Maps table names to info about what to which _sync_time column to set
+ * to NULL on an update. This is used to support syncing. */
+ private final Map<String, SyncUpdateInfo> mSyncUpdateInfo =
+ new HashMap<String, SyncUpdateInfo>();
+
+ public Map<String, String> getSyncedTables() {
+ synchronized(mSyncUpdateInfo) {
+ HashMap<String, String> tables = new HashMap<String, String>();
+ for (String table : mSyncUpdateInfo.keySet()) {
+ SyncUpdateInfo info = mSyncUpdateInfo.get(table);
+ if (info.deletedTable != null) {
+ tables.put(table, info.deletedTable);
+ }
+ }
+ return tables;
+ }
+ }
+
+ /**
+ * Internal class used to keep track what needs to be marked as changed
+ * when an update occurs. This is used for syncing, so the sync engine
+ * knows what data has been updated locally.
+ */
+ static private class SyncUpdateInfo {
+ /**
+ * Creates the SyncUpdateInfo class.
+ *
+ * @param masterTable The table to set _sync_time to NULL in
+ * @param deletedTable The deleted table that corresponds to the
+ * master table
+ * @param foreignKey The key that refers to the primary key in table
+ */
+ SyncUpdateInfo(String masterTable, String deletedTable,
+ String foreignKey) {
+ this.masterTable = masterTable;
+ this.deletedTable = deletedTable;
+ this.foreignKey = foreignKey;
+ }
+
+ /** The table containing the _sync_time column */
+ String masterTable;
+
+ /** The deleted table that corresponds to the master table */
+ String deletedTable;
+
+ /** The key in the local table the row in table. It may be _id, if table
+ * is the local table. */
+ String foreignKey;
+ }
+
+ /**
+ * Used to allow returning sub-classes of {@link Cursor} when calling query.
+ */
+ public interface CursorFactory {
+ /**
+ * See
+ * {@link SQLiteCursor#SQLiteCursor(SQLiteDatabase, SQLiteCursorDriver,
+ * String, SQLiteQuery)}.
+ */
+ public Cursor newCursor(SQLiteDatabase db,
+ SQLiteCursorDriver masterQuery, String editTable,
+ SQLiteQuery query);
+ }
+
+ /**
+ * Open the database according to the flags {@link #OPEN_READWRITE}
+ * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}.
+ *
+ * <p>Sets the locale of the database to the the system's current locale.
+ * Call {@link #setLocale} if you would like something else.</p>
+ *
+ * @param path to database file to open and/or create
+ * @param factory an optional factory class that is called to instantiate a
+ * cursor when query is called, or null for default
+ * @param flags to control database access mode
+ * @return the newly opened database
+ * @throws SQLiteException if the database cannot be opened
+ */
+ public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags) {
+ SQLiteDatabase db = null;
+ try {
+ // Open the database.
+ return new SQLiteDatabase(path, factory, flags);
+ } catch (SQLiteDatabaseCorruptException e) {
+ // Try to recover from this, if we can.
+ // TODO: should we do this for other open failures?
+ Log.e(TAG, "Deleting and re-creating corrupt database " + path, e);
+ new File(path).delete();
+ return new SQLiteDatabase(path, factory, flags);
+ }
+ }
+
+ /**
+ * Equivalent to openDatabase(file.getPath(), factory, CREATE_IF_NECESSARY).
+ */
+ public static SQLiteDatabase openOrCreateDatabase(File file, CursorFactory factory) {
+ return openOrCreateDatabase(file.getPath(), factory);
+ }
+
+ /**
+ * Equivalent to openDatabase(path, factory, CREATE_IF_NECESSARY).
+ */
+ public static SQLiteDatabase openOrCreateDatabase(String path, CursorFactory factory) {
+ return openDatabase(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 factory an optional factory class that is called to instantiate a
+ * cursor when query is called
+ * @return a SQLiteDatabase object, or null if the database can't be created
+ */
+ public static SQLiteDatabase create(CursorFactory factory) {
+ // This is a magic string with special meaning for SQLite.
+ return openDatabase(":memory:", factory, CREATE_IF_NECESSARY);
+ }
+
+ /**
+ * Close the database.
+ */
+ public void close() {
+ lock();
+ try {
+ closeClosable();
+ releaseReference();
+ } finally {
+ unlock();
+ }
+ }
+
+ private void closeClosable() {
+ Iterator<Map.Entry<SQLiteClosable, Object>> iter = mPrograms.entrySet().iterator();
+ while (iter.hasNext()) {
+ Map.Entry<SQLiteClosable, Object> entry = iter.next();
+ SQLiteClosable program = entry.getKey();
+ if (program != null) {
+ program.onAllReferencesReleasedFromContainer();
+ }
+ }
+ }
+
+ /**
+ * Native call to close the database.
+ */
+ private native void dbclose();
+
+ /**
+ * Gets the database version.
+ *
+ * @return the database version
+ */
+ public int getVersion() {
+ SQLiteStatement prog = null;
+ lock();
+ try {
+ prog = new SQLiteStatement(this, "PRAGMA user_version;");
+ long version = prog.simpleQueryForLong();
+ return (int) version;
+ } finally {
+ if (prog != null) prog.close();
+ unlock();
+ }
+ }
+
+ /**
+ * Sets the database version.
+ *
+ * @param version the new database version
+ */
+ public void setVersion(int version) {
+ execSQL("PRAGMA user_version = " + version);
+ }
+
+ /**
+ * Returns the maximum size the database may grow to.
+ *
+ * @return the new maximum database size
+ */
+ public long getMaximumSize() {
+ SQLiteStatement prog = null;
+ lock();
+ try {
+ prog = new SQLiteStatement(this,
+ "PRAGMA max_page_count;");
+ long pageCount = prog.simpleQueryForLong();
+ return pageCount * getPageSize();
+ } finally {
+ if (prog != null) prog.close();
+ unlock();
+ }
+ }
+
+ /**
+ * Sets the maximum size the database will grow to. The maximum size cannot
+ * be set below the current size.
+ *
+ * @param numBytes the maximum database size, in bytes
+ * @return the new maximum database size
+ */
+ public long setMaximumSize(long numBytes) {
+ SQLiteStatement prog = null;
+ lock();
+ try {
+ long pageSize = getPageSize();
+ long numPages = numBytes / pageSize;
+ // If numBytes isn't a multiple of pageSize, bump up a page
+ if ((numBytes % pageSize) != 0) {
+ numPages++;
+ }
+ prog = new SQLiteStatement(this,
+ "PRAGMA max_page_count = " + numPages);
+ long newPageCount = prog.simpleQueryForLong();
+ return newPageCount * pageSize;
+ } finally {
+ if (prog != null) prog.close();
+ unlock();
+ }
+ }
+
+ /**
+ * Returns the current database page size, in bytes.
+ *
+ * @return the database page size, in bytes
+ */
+ public long getPageSize() {
+ SQLiteStatement prog = null;
+ lock();
+ try {
+ prog = new SQLiteStatement(this,
+ "PRAGMA page_size;");
+ long size = prog.simpleQueryForLong();
+ return size;
+ } finally {
+ if (prog != null) prog.close();
+ unlock();
+ }
+ }
+
+ /**
+ * Sets the database page size. The page size must be a power of two. This
+ * method does not work if any data has been written to the database file,
+ * and must be called right after the database has been created.
+ *
+ * @param numBytes the database page size, in bytes
+ */
+ public void setPageSize(long numBytes) {
+ execSQL("PRAGMA page_size = " + numBytes);
+ }
+
+ /**
+ * Mark this table as syncable. When an update occurs in this table the
+ * _sync_dirty field will be set to ensure proper syncing operation.
+ *
+ * @param table the table to mark as syncable
+ * @param deletedTable The deleted table that corresponds to the
+ * syncable table
+ */
+ public void markTableSyncable(String table, String deletedTable) {
+ markTableSyncable(table, "_id", table, deletedTable);
+ }
+
+ /**
+ * Mark this table as syncable, with the _sync_dirty residing in another
+ * table. When an update occurs in this table the _sync_dirty field of the
+ * row in updateTable with the _id in foreignKey will be set to
+ * ensure proper syncing operation.
+ *
+ * @param table an update on this table will trigger a sync time removal
+ * @param foreignKey this is the column in table whose value is an _id in
+ * updateTable
+ * @param updateTable this is the table that will have its _sync_dirty
+ */
+ public void markTableSyncable(String table, String foreignKey,
+ String updateTable) {
+ markTableSyncable(table, foreignKey, updateTable, null);
+ }
+
+ /**
+ * Mark this table as syncable, with the _sync_dirty residing in another
+ * table. When an update occurs in this table the _sync_dirty field of the
+ * row in updateTable with the _id in foreignKey will be set to
+ * ensure proper syncing operation.
+ *
+ * @param table an update on this table will trigger a sync time removal
+ * @param foreignKey this is the column in table whose value is an _id in
+ * updateTable
+ * @param updateTable this is the table that will have its _sync_dirty
+ * @param deletedTable The deleted table that corresponds to the
+ * updateTable
+ */
+ private void markTableSyncable(String table, String foreignKey,
+ String updateTable, String deletedTable) {
+ lock();
+ try {
+ native_execSQL("SELECT _sync_dirty FROM " + updateTable
+ + " LIMIT 0");
+ native_execSQL("SELECT " + foreignKey + " FROM " + table
+ + " LIMIT 0");
+ } finally {
+ unlock();
+ }
+
+ SyncUpdateInfo info = new SyncUpdateInfo(updateTable, deletedTable,
+ foreignKey);
+ synchronized (mSyncUpdateInfo) {
+ mSyncUpdateInfo.put(table, info);
+ }
+ }
+
+ /**
+ * Call for each row that is updated in a cursor.
+ *
+ * @param table the table the row is in
+ * @param rowId the row ID of the updated row
+ */
+ /* package */ void rowUpdated(String table, long rowId) {
+ SyncUpdateInfo info;
+ synchronized (mSyncUpdateInfo) {
+ info = mSyncUpdateInfo.get(table);
+ }
+ if (info != null) {
+ execSQL("UPDATE " + info.masterTable
+ + " SET _sync_dirty=1 WHERE _id=(SELECT " + info.foreignKey
+ + " FROM " + table + " WHERE _id=" + rowId + ")");
+ }
+ }
+
+ /**
+ * Finds the name of the first table, which is editable.
+ *
+ * @param tables a list of tables
+ * @return the first table listed
+ */
+ public static String findEditTable(String tables) {
+ if (!TextUtils.isEmpty(tables)) {
+ // find the first word terminated by either a space or a comma
+ int spacepos = tables.indexOf(' ');
+ int commapos = tables.indexOf(',');
+
+ if (spacepos > 0 && (spacepos < commapos || commapos < 0)) {
+ return tables.substring(0, spacepos);
+ } else if (commapos > 0 && (commapos < spacepos || spacepos < 0) ) {
+ return tables.substring(0, commapos);
+ }
+ return tables;
+ } else {
+ throw new IllegalStateException("Invalid tables");
+ }
+ }
+
+ /**
+ * Compiles an SQL statement into a reusable pre-compiled statement object.
+ * The parameters are identical to {@link #execSQL(String)}. You may put ?s in the
+ * statement and fill in those values with {@link SQLiteProgram#bindString}
+ * and {@link SQLiteProgram#bindLong} each time you want to run the
+ * statement. Statements may not return result sets larger than 1x1.
+ *
+ * @param sql The raw SQL statement, may contain ? for unknown values to be
+ * bound later.
+ * @return a pre-compiled statement object.
+ */
+ public SQLiteStatement compileStatement(String sql) throws SQLException {
+ lock();
+ try {
+ return new SQLiteStatement(this, sql);
+ } finally {
+ unlock();
+ }
+ }
+
+ /**
+ * Query the given URL, returning a {@link Cursor} over the result set.
+ *
+ * @param distinct true if you want each row to be unique, false otherwise.
+ * @param table The table name to compile the query against.
+ * @param columns A list of which columns to return. Passing null will
+ * return all columns, which is discouraged to prevent reading
+ * data from storage that isn't going to be used.
+ * @param selection A filter declaring which rows to return, formatted as an
+ * SQL WHERE clause (excluding the WHERE itself). Passing null
+ * will return all rows for the given table.
+ * @param selectionArgs You may include ?s in selection, which will be
+ * replaced by the values from selectionArgs, in order that they
+ * appear in the selection. The values will be bound as Strings.
+ * @param groupBy A filter declaring how to group rows, formatted as an SQL
+ * GROUP BY clause (excluding the GROUP BY itself). Passing null
+ * will cause the rows to not be grouped.
+ * @param having A filter declare which row groups to include in the cursor,
+ * if row grouping is being used, formatted as an SQL HAVING
+ * clause (excluding the HAVING itself). Passing null will cause
+ * all row groups to be included, and is required when row
+ * grouping is not being used.
+ * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause
+ * (excluding the ORDER BY itself). Passing null will use the
+ * default sort order, which may be unordered.
+ * @param limit Limits the number of rows returned by the query,
+ * formatted as LIMIT clause. Passing null denotes no LIMIT clause.
+ * @return A Cursor object, which is positioned before the first entry
+ * @see Cursor
+ */
+ public Cursor query(boolean distinct, String table, String[] columns,
+ String selection, String[] selectionArgs, String groupBy,
+ String having, String orderBy, String limit) {
+ return queryWithFactory(null, distinct, table, columns, selection, selectionArgs,
+ groupBy, having, orderBy, limit);
+ }
+
+ /**
+ * Query the given URL, returning a {@link Cursor} over the result set.
+ *
+ * @param cursorFactory the cursor factory to use, or null for the default factory
+ * @param distinct true if you want each row to be unique, false otherwise.
+ * @param table The table name to compile the query against.
+ * @param columns A list of which columns to return. Passing null will
+ * return all columns, which is discouraged to prevent reading
+ * data from storage that isn't going to be used.
+ * @param selection A filter declaring which rows to return, formatted as an
+ * SQL WHERE clause (excluding the WHERE itself). Passing null
+ * will return all rows for the given table.
+ * @param selectionArgs You may include ?s in selection, which will be
+ * replaced by the values from selectionArgs, in order that they
+ * appear in the selection. The values will be bound as Strings.
+ * @param groupBy A filter declaring how to group rows, formatted as an SQL
+ * GROUP BY clause (excluding the GROUP BY itself). Passing null
+ * will cause the rows to not be grouped.
+ * @param having A filter declare which row groups to include in the cursor,
+ * if row grouping is being used, formatted as an SQL HAVING
+ * clause (excluding the HAVING itself). Passing null will cause
+ * all row groups to be included, and is required when row
+ * grouping is not being used.
+ * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause
+ * (excluding the ORDER BY itself). Passing null will use the
+ * default sort order, which may be unordered.
+ * @param limit Limits the number of rows returned by the query,
+ * formatted as LIMIT clause. Passing null denotes no LIMIT clause.
+ * @return A Cursor object, which is positioned before the first entry
+ * @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) {
+ String sql = SQLiteQueryBuilder.buildQueryString(
+ distinct, table, columns, selection, groupBy, having, orderBy, limit);
+
+ return rawQueryWithFactory(
+ cursorFactory, sql, selectionArgs, findEditTable(table));
+ }
+
+ /**
+ * Query the given table, returning a {@link Cursor} over the result set.
+ *
+ * @param table The table name to compile the query against.
+ * @param columns A list of which columns to return. Passing null will
+ * return all columns, which is discouraged to prevent reading
+ * data from storage that isn't going to be used.
+ * @param selection A filter declaring which rows to return, formatted as an
+ * SQL WHERE clause (excluding the WHERE itself). Passing null
+ * will return all rows for the given table.
+ * @param selectionArgs You may include ?s in selection, which will be
+ * replaced by the values from selectionArgs, in order that they
+ * appear in the selection. The values will be bound as Strings.
+ * @param groupBy A filter declaring how to group rows, formatted as an SQL
+ * GROUP BY clause (excluding the GROUP BY itself). Passing null
+ * will cause the rows to not be grouped.
+ * @param having A filter declare which row groups to include in the cursor,
+ * if row grouping is being used, formatted as an SQL HAVING
+ * clause (excluding the HAVING itself). Passing null will cause
+ * all row groups to be included, and is required when row
+ * grouping is not being used.
+ * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause
+ * (excluding the ORDER BY itself). Passing null will use the
+ * default sort order, which may be unordered.
+ * @return A {@link Cursor} object, which is positioned before the first entry
+ * @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
+ * @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
+ */
+ public Cursor rawQuery(String sql, String[] selectionArgs) {
+ return rawQueryWithFactory(null, sql, selectionArgs, null);
+ }
+
+ /**
+ * Runs the provided SQL and returns a cursor over the result set.
+ *
+ * @param cursorFactory the cursor factory to use, or null for the default factory
+ * @param sql the SQL query. The SQL string must not be ; terminated
+ * @param selectionArgs You may include ?s in where clause in the query,
+ * which will be replaced by the values from selectionArgs. The
+ * values will be bound as Strings.
+ * @param editTable the name of the first table, which is editable
+ * @return A {@link Cursor} object, which is positioned before the first entry
+ */
+ public Cursor rawQueryWithFactory(
+ CursorFactory cursorFactory, String sql, String[] selectionArgs,
+ String editTable) {
+ long timeStart = 0;
+
+ if (Config.LOGV) {
+ timeStart = System.currentTimeMillis();
+ }
+
+ SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable);
+
+ try {
+ return driver.query(
+ cursorFactory != null ? cursorFactory : mFactory,
+ selectionArgs);
+ } finally {
+ if (Config.LOGV) {
+ long duration = System.currentTimeMillis() - timeStart;
+
+ Log.v(SQLiteCursor.TAG,
+ "query (" + duration + " ms): " + driver.toString() + ", args are "
+ + (selectionArgs != null
+ ? TextUtils.join(",", selectionArgs)
+ : "<null>"));
+ }
+ }
+ }
+
+ /**
+ * Runs the provided SQL and returns a cursor over the result set.
+ * The cursor will read an initial set of rows and the return to the caller.
+ * It will continue to read in batches and send data changed notifications
+ * when the later batches are ready.
+ * @param sql the SQL query. The SQL string must not be ; terminated
+ * @param selectionArgs You may include ?s in where clause in the query,
+ * which will be replaced by the values from selectionArgs. The
+ * values will be bound as Strings.
+ * @param initialRead set the initial count of items to read from the cursor
+ * @param maxRead set the count of items to read on each iteration after the first
+ * @return A {@link Cursor} object, which is positioned before the first entry
+ * @hide pending API council approval
+ */
+ public Cursor rawQuery(String sql, String[] selectionArgs,
+ int initialRead, int maxRead) {
+ SQLiteCursor c = (SQLiteCursor)rawQueryWithFactory(
+ null, sql, selectionArgs, null);
+ c.setLoadStyle(initialRead, maxRead);
+ return c;
+ }
+
+ /**
+ * Convenience method for inserting a row into the database.
+ *
+ * @param table the table to insert the row into
+ * @param nullColumnHack SQL doesn't allow inserting a completely empty row,
+ * so if initialValues is empty this column will explicitly be
+ * assigned a NULL value
+ * @param values this map contains the initial column values for the
+ * row. The keys should be the column names and the values the
+ * column values
+ * @return the row ID of the newly inserted row, or -1 if an error occurred
+ */
+ public long insert(String table, String nullColumnHack, ContentValues values) {
+ try {
+ return insertWithOnConflict(table, nullColumnHack, values, null);
+ } 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 SQL doesn't allow inserting a completely empty row,
+ * so if initialValues is empty this column will explicitly be
+ * assigned a NULL value
+ * @param values this map contains the initial column values for the
+ * row. The keys should be the column names and the values the
+ * column values
+ * @throws SQLException
+ * @return the row ID of the newly inserted row, or -1 if an error occurred
+ */
+ public long insertOrThrow(String table, String nullColumnHack, ContentValues values)
+ throws SQLException {
+ return insertWithOnConflict(table, nullColumnHack, values, null);
+ }
+
+ /**
+ * Convenience method for replacing a row in the database.
+ *
+ * @param table the table in which to replace the row
+ * @param nullColumnHack SQL doesn't allow inserting a completely empty row,
+ * so if initialValues is empty this row will explicitly be
+ * assigned a NULL value
+ * @param initialValues this map contains the initial column values for
+ * the row. The key
+ * @return the row ID of the newly inserted row, or -1 if an error occurred
+ */
+ public long replace(String table, String nullColumnHack, ContentValues initialValues) {
+ try {
+ return insertWithOnConflict(table, nullColumnHack, initialValues,
+ ConflictAlgorithm.REPLACE);
+ } catch (SQLException e) {
+ Log.e(TAG, "Error inserting " + initialValues, e);
+ return -1;
+ }
+ }
+
+ /**
+ * Convenience method for replacing a row in the database.
+ *
+ * @param table the table in which to replace the row
+ * @param nullColumnHack SQL doesn't allow inserting a completely empty row,
+ * so if initialValues is empty this row will explicitly be
+ * assigned a NULL value
+ * @param initialValues this map contains the initial column values for
+ * the row. The key
+ * @throws SQLException
+ * @return the row ID of the newly inserted row, or -1 if an error occurred
+ */
+ public long replaceOrThrow(String table, String nullColumnHack,
+ ContentValues initialValues) throws SQLException {
+ return insertWithOnConflict(table, nullColumnHack, initialValues,
+ ConflictAlgorithm.REPLACE);
+ }
+
+ /**
+ * General method for inserting a row into the database.
+ *
+ * @param table the table to insert the row into
+ * @param nullColumnHack SQL doesn't allow inserting a completely empty row,
+ * so if initialValues is empty this column will explicitly be
+ * assigned a NULL value
+ * @param initialValues this map contains the initial column values for the
+ * row. The keys should be the column names and the values the
+ * column values
+ * @param algorithm {@link ConflictAlgorithm} for insert conflict resolver
+ * @return the row ID of the newly inserted row, or -1 if an error occurred
+ * @hide
+ */
+ public long insertWithOnConflict(String table, String nullColumnHack,
+ ContentValues initialValues, ConflictAlgorithm algorithm) {
+ if (!isOpen()) {
+ throw new IllegalStateException("database not open");
+ }
+
+ // Measurements show most sql lengths <= 152
+ StringBuilder sql = new StringBuilder(152);
+ sql.append("INSERT");
+ if (algorithm != null) {
+ sql.append(" OR ");
+ sql.append(algorithm.value());
+ }
+ sql.append(" INTO ");
+ sql.append(table);
+ // Measurements show most values lengths < 40
+ StringBuilder values = new StringBuilder(40);
+
+ Set<Map.Entry<String, Object>> entrySet = null;
+ if (initialValues != null && initialValues.size() > 0) {
+ entrySet = initialValues.valueSet();
+ Iterator<Map.Entry<String, Object>> entriesIter = entrySet.iterator();
+ sql.append('(');
+
+ boolean needSeparator = false;
+ while (entriesIter.hasNext()) {
+ if (needSeparator) {
+ sql.append(", ");
+ values.append(", ");
+ }
+ needSeparator = true;
+ Map.Entry<String, Object> entry = entriesIter.next();
+ sql.append(entry.getKey());
+ values.append('?');
+ }
+
+ sql.append(')');
+ } else {
+ sql.append("(" + nullColumnHack + ") ");
+ values.append("NULL");
+ }
+
+ sql.append(" VALUES(");
+ sql.append(values);
+ sql.append(");");
+
+ lock();
+ SQLiteStatement statement = null;
+ try {
+ statement = compileStatement(sql.toString());
+
+ // Bind the values
+ if (entrySet != null) {
+ int size = entrySet.size();
+ Iterator<Map.Entry<String, Object>> entriesIter = entrySet.iterator();
+ for (int i = 0; i < size; i++) {
+ Map.Entry<String, Object> entry = entriesIter.next();
+ DatabaseUtils.bindObjectToProgram(statement, i + 1, entry.getValue());
+ }
+ }
+
+ // Run the program and then cleanup
+ statement.execute();
+
+ long insertedRowId = lastInsertRow();
+ if (insertedRowId == -1) {
+ Log.e(TAG, "Error inserting " + initialValues + " using " + sql);
+ } else {
+ if (Config.LOGD && Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Inserting row " + insertedRowId + " from "
+ + initialValues + " using " + sql);
+ }
+ }
+ return insertedRowId;
+ } catch (SQLiteDatabaseCorruptException e) {
+ onCorruption();
+ throw e;
+ } finally {
+ if (statement != null) {
+ statement.close();
+ }
+ unlock();
+ }
+ }
+
+ /**
+ * Convenience method for deleting rows in the database.
+ *
+ * @param table the table to delete from
+ * @param whereClause the optional WHERE clause to apply when deleting.
+ * Passing null will delete all rows.
+ * @return the number of rows affected if a whereClause is passed in, 0
+ * otherwise. To remove all rows and get a count pass "1" as the
+ * whereClause.
+ */
+ public int delete(String table, String whereClause, String[] whereArgs) {
+ if (!isOpen()) {
+ throw new IllegalStateException("database not open");
+ }
+ lock();
+ SQLiteStatement statement = null;
+ try {
+ statement = compileStatement("DELETE FROM " + table
+ + (!TextUtils.isEmpty(whereClause)
+ ? " WHERE " + whereClause : ""));
+ if (whereArgs != null) {
+ int numArgs = whereArgs.length;
+ for (int i = 0; i < numArgs; i++) {
+ DatabaseUtils.bindObjectToProgram(statement, i + 1, whereArgs[i]);
+ }
+ }
+ statement.execute();
+ statement.close();
+ return lastChangeCount();
+ } catch (SQLiteDatabaseCorruptException e) {
+ onCorruption();
+ throw e;
+ } finally {
+ if (statement != null) {
+ statement.close();
+ }
+ unlock();
+ }
+ }
+
+ /**
+ * Convenience method for updating rows in the database.
+ *
+ * @param table the table to update in
+ * @param values a map from column names to new column values. null is a
+ * valid value that will be translated to NULL.
+ * @param whereClause the optional WHERE clause to apply when updating.
+ * Passing null will update all rows.
+ * @return the number of rows affected
+ */
+ public int update(String table, ContentValues values, String whereClause, String[] whereArgs) {
+ return updateWithOnConflict(table, values, whereClause, whereArgs, null);
+ }
+
+ /**
+ * 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 algorithm {@link ConflictAlgorithm} for update conflict resolver
+ * @return the number of rows affected
+ * @hide
+ */
+ public int updateWithOnConflict(String table, ContentValues values,
+ String whereClause, String[] whereArgs, ConflictAlgorithm algorithm) {
+ if (!isOpen()) {
+ throw new IllegalStateException("database not open");
+ }
+
+ if (values == null || values.size() == 0) {
+ throw new IllegalArgumentException("Empty values");
+ }
+
+ StringBuilder sql = new StringBuilder(120);
+ sql.append("UPDATE ");
+ if (algorithm != null) {
+ sql.append(" OR ");
+ sql.append(algorithm.value());
+ }
+
+ sql.append(table);
+ sql.append(" SET ");
+
+ Set<Map.Entry<String, Object>> entrySet = values.valueSet();
+ Iterator<Map.Entry<String, Object>> entriesIter = entrySet.iterator();
+
+ while (entriesIter.hasNext()) {
+ Map.Entry<String, Object> entry = entriesIter.next();
+ sql.append(entry.getKey());
+ sql.append("=?");
+ if (entriesIter.hasNext()) {
+ sql.append(", ");
+ }
+ }
+
+ if (!TextUtils.isEmpty(whereClause)) {
+ sql.append(" WHERE ");
+ sql.append(whereClause);
+ }
+
+ lock();
+ SQLiteStatement statement = null;
+ try {
+ statement = compileStatement(sql.toString());
+
+ // Bind the values
+ int size = entrySet.size();
+ entriesIter = entrySet.iterator();
+ int bindArg = 1;
+ for (int i = 0; i < size; i++) {
+ Map.Entry<String, Object> entry = entriesIter.next();
+ DatabaseUtils.bindObjectToProgram(statement, bindArg, entry.getValue());
+ bindArg++;
+ }
+
+ if (whereArgs != null) {
+ size = whereArgs.length;
+ for (int i = 0; i < size; i++) {
+ statement.bindString(bindArg, whereArgs[i]);
+ bindArg++;
+ }
+ }
+
+ // Run the program and then cleanup
+ statement.execute();
+ statement.close();
+ int numChangedRows = lastChangeCount();
+ if (Config.LOGD && Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Updated " + numChangedRows + " using " + values + " and " + sql);
+ }
+ return numChangedRows;
+ } catch (SQLiteDatabaseCorruptException e) {
+ onCorruption();
+ throw e;
+ } catch (SQLException e) {
+ Log.e(TAG, "Error updating " + values + " using " + sql);
+ throw e;
+ } finally {
+ if (statement != null) {
+ statement.close();
+ }
+ unlock();
+ }
+ }
+
+ /**
+ * Execute a single SQL statement that is not a query. For example, CREATE
+ * TABLE, DELETE, INSERT, etc. Multiple statements separated by ;s are not
+ * supported. it takes a write lock
+ *
+ * @throws SQLException If the SQL string is invalid for some reason
+ */
+ public void execSQL(String sql) throws SQLException {
+ boolean logStats = mLogStats;
+ long timeStart = logStats ? SystemClock.elapsedRealtime() : 0;
+ lock();
+ try {
+ native_execSQL(sql);
+ } catch (SQLiteDatabaseCorruptException e) {
+ onCorruption();
+ throw e;
+ } finally {
+ unlock();
+ }
+ if (logStats) {
+ logTimeStat(false /* not a read */, timeStart, SystemClock.elapsedRealtime());
+ }
+ }
+
+ /**
+ * Execute a single SQL statement that is not a query. For example, CREATE
+ * TABLE, DELETE, INSERT, etc. Multiple statements separated by ;s are not
+ * supported. it takes a write lock,
+ *
+ * @param sql
+ * @param bindArgs only byte[], String, Long and Double are supported in bindArgs.
+ * @throws SQLException If the SQL string is invalid for some reason
+ */
+ public void execSQL(String sql, Object[] bindArgs) throws SQLException {
+ if (bindArgs == null) {
+ throw new IllegalArgumentException("Empty bindArgs");
+ }
+
+ boolean logStats = mLogStats;
+ long timeStart = logStats ? SystemClock.elapsedRealtime() : 0;
+ lock();
+ SQLiteStatement statement = null;
+ try {
+ statement = compileStatement(sql);
+ if (bindArgs != null) {
+ int numArgs = bindArgs.length;
+ for (int i = 0; i < numArgs; i++) {
+ DatabaseUtils.bindObjectToProgram(statement, i + 1, bindArgs[i]);
+ }
+ }
+ statement.execute();
+ } catch (SQLiteDatabaseCorruptException e) {
+ onCorruption();
+ throw e;
+ } finally {
+ if (statement != null) {
+ statement.close();
+ }
+ unlock();
+ }
+ if (logStats) {
+ logTimeStat(false /* not a read */, timeStart, SystemClock.elapsedRealtime());
+ }
+ }
+
+ @Override
+ protected void finalize() {
+ if (isOpen()) {
+ if (mPrograms.isEmpty()) {
+ Log.e(TAG, "Leak found", mLeakedException);
+ } else {
+ IllegalStateException leakProgram = new IllegalStateException(
+ "mPrograms size " + mPrograms.size(), mLeakedException);
+ Log.e(TAG, "Leak found", leakProgram);
+ }
+ closeClosable();
+ onAllReferencesReleased();
+ }
+ }
+
+ /**
+ * Private constructor. See {@link #create} and {@link #openDatabase}.
+ *
+ * @param path The full path to the database
+ * @param factory The factory to use when creating cursors, may be NULL.
+ * @param flags 0 or {@link #NO_LOCALIZED_COLLATORS}. If the database file already
+ * exists, mFlags will be updated appropriately.
+ */
+ private SQLiteDatabase(String path, CursorFactory factory, int flags) {
+ if (path == null) {
+ throw new IllegalArgumentException("path should not be null");
+ }
+ mFlags = flags;
+ mPath = path;
+ mLogStats = "1".equals(android.os.SystemProperties.get("db.logstats"));
+
+ mLeakedException = new IllegalStateException(path +
+ " SQLiteDatabase created and never closed");
+ mFactory = factory;
+ dbopen(mPath, mFlags);
+ mPrograms = new WeakHashMap<SQLiteClosable,Object>();
+ try {
+ setLocale(Locale.getDefault());
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Failed to setLocale() when constructing, closing the database", e);
+ dbclose();
+ throw e;
+ }
+ }
+
+ /**
+ * return whether the DB is opened as read only.
+ * @return true if DB is opened as read only
+ */
+ public boolean isReadOnly() {
+ return (mFlags & OPEN_READ_MASK) == OPEN_READONLY;
+ }
+
+ /**
+ * @return true if the DB is currently open (has not been closed)
+ */
+ public boolean isOpen() {
+ return mNativeHandle != 0;
+ }
+
+ public boolean needUpgrade(int newVersion) {
+ return newVersion > getVersion();
+ }
+
+ /**
+ * Getter for the path to the database file.
+ *
+ * @return the path to our database file.
+ */
+ public final String getPath() {
+ return mPath;
+ }
+
+ /* package */ void logTimeStat(boolean read, long begin, long end) {
+ EventLog.writeEvent(DB_OPERATION_EVENT, mPath, read ? 0 : 1, end - begin);
+ }
+
+ /**
+ * Sets the locale for this database. Does nothing if this database has
+ * the NO_LOCALIZED_COLLATORS flag set or was opened read only.
+ * @throws SQLException if the locale could not be set. The most common reason
+ * for this is that there is no collator available for the locale you requested.
+ * In this case the database remains unchanged.
+ */
+ public void setLocale(Locale locale) {
+ lock();
+ try {
+ native_setLocale(locale.toString(), mFlags);
+ } finally {
+ unlock();
+ }
+ }
+
+ /**
+ * Native call to open the database.
+ *
+ * @param path The full path to the database
+ */
+ private native void dbopen(String path, int flags);
+
+ /**
+ * Native call to execute a raw SQL statement. {@link #lock} must be held
+ * when calling this method.
+ *
+ * @param sql The raw SQL string
+ * @throws SQLException
+ */
+ /* package */ native void native_execSQL(String sql) throws SQLException;
+
+ /**
+ * Native call to set the locale. {@link #lock} must be held when calling
+ * this method.
+ * @throws SQLException
+ */
+ /* package */ native void native_setLocale(String loc, int flags);
+
+ /**
+ * Returns the row ID of the last row inserted into the database.
+ *
+ * @return the row ID of the last row inserted into the database.
+ */
+ /* package */ native long lastInsertRow();
+
+ /**
+ * Returns the number of changes made in the last statement executed.
+ *
+ * @return the number of changes made in the last statement executed.
+ */
+ /* package */ native int lastChangeCount();
+}
diff --git a/core/java/android/database/sqlite/SQLiteDatabaseCorruptException.java b/core/java/android/database/sqlite/SQLiteDatabaseCorruptException.java
new file mode 100644
index 0000000..73b6c0c
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteDatabaseCorruptException.java
@@ -0,0 +1,28 @@
+/*
+ * 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);
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteDebug.java b/core/java/android/database/sqlite/SQLiteDebug.java
new file mode 100644
index 0000000..d04afb0
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteDebug.java
@@ -0,0 +1,107 @@
+/*
+ * 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.util.Config;
+
+/**
+ * Provides debugging info about all SQLite databases running in the current process.
+ *
+ * {@hide}
+ */
+public final class SQLiteDebug {
+ /**
+ * Controls the printing of SQL statements as they are executed.
+ */
+ public static final boolean DEBUG_SQL_STATEMENTS = Config.LOGV;
+
+ /**
+ * Controls the stack trace reporting of active cursors being
+ * finalized.
+ */
+ public static final boolean DEBUG_ACTIVE_CURSOR_FINALIZATION = Config.LOGV;
+
+ /**
+ * Controls the tracking of time spent holding the database lock.
+ */
+ public static final boolean DEBUG_LOCK_TIME_TRACKING = false;
+
+ /**
+ * Controls the printing of stack traces when tracking the time spent holding the database lock.
+ */
+ public static final boolean DEBUG_LOCK_TIME_TRACKING_STACK_TRACE = false;
+
+ /**
+ * Contains statistics about the active pagers in the current process.
+ *
+ * @see #getPagerStats(PagerStats)
+ */
+ public static class PagerStats {
+ /** The total number of bytes in all pagers in the current process */
+ public long totalBytes;
+ /** The number of bytes in referenced pages in all pagers in the current process */
+ public long referencedBytes;
+ /** The number of bytes in all database files opened in the current process */
+ public long databaseBytes;
+ /** The number of pagers opened in the current process */
+ public int numPagers;
+ }
+
+ /**
+ * Gathers statistics about all pagers in the current process.
+ */
+ public static native void getPagerStats(PagerStats stats);
+
+ /**
+ * Returns the size of the SQLite heap.
+ * @return The size of the SQLite heap in bytes.
+ */
+ public static native long getHeapSize();
+
+ /**
+ * Returns the amount of allocated memory in the SQLite heap.
+ * @return The allocated size in bytes.
+ */
+ public static native long getHeapAllocatedSize();
+
+ /**
+ * Returns the amount of free memory in the SQLite heap.
+ * @return The freed size in bytes.
+ */
+ public static native long getHeapFreeSize();
+
+ /**
+ * Determines the number of dirty belonging to the SQLite
+ * heap segments of this process. pages[0] returns the number of
+ * shared pages, pages[1] returns the number of private pages
+ */
+ public static native void getHeapDirtyPages(int[] pages);
+
+ private static int sNumActiveCursorsFinalized = 0;
+
+ /**
+ * Returns the number of active cursors that have been finalized. This depends on the GC having
+ * run but is still useful for tests.
+ */
+ public static int getNumActiveCursorsFinalized() {
+ return sNumActiveCursorsFinalized;
+ }
+
+ static synchronized void notifyActiveCursorFinalized() {
+ sNumActiveCursorsFinalized++;
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java
new file mode 100644
index 0000000..ca64aca
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java
@@ -0,0 +1,97 @@
+/*
+ * 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;
+import android.util.Log;
+
+/**
+ * A cursor driver that uses the given query directly.
+ *
+ * @hide
+ */
+public class SQLiteDirectCursorDriver implements SQLiteCursorDriver {
+ private static String TAG = "SQLiteDirectCursorDriver";
+ private String mEditTable;
+ private SQLiteDatabase mDatabase;
+ private Cursor mCursor;
+ private String mSql;
+ private SQLiteQuery mQuery;
+
+ public SQLiteDirectCursorDriver(SQLiteDatabase db, String sql, String editTable) {
+ mDatabase = db;
+ mEditTable = editTable;
+ //TODO remove all callers that end in ; and remove this check
+ if (sql.charAt(sql.length() - 1) == ';') {
+ Log.w(TAG, "Found SQL string that ends in ; -- " + sql);
+ sql = sql.substring(0, sql.length() - 1);
+ }
+ mSql = sql;
+ }
+
+ public Cursor query(CursorFactory factory, String[] selectionArgs) {
+ // Compile the query
+ SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, 0, selectionArgs);
+
+ try {
+ // Arg binding
+ int numArgs = selectionArgs == null ? 0 : selectionArgs.length;
+ for (int i = 0; i < numArgs; i++) {
+ query.bindString(i + 1, selectionArgs[i]);
+ }
+
+ // Create the cursor
+ if (factory == null) {
+ mCursor = new SQLiteCursor(mDatabase, this, mEditTable, query);
+ } else {
+ mCursor = factory.newCursor(mDatabase, this, mEditTable, query);
+ }
+
+ mQuery = query;
+ query = null;
+ return mCursor;
+ } finally {
+ // Make sure this object is cleaned up if something happens
+ if (query != null) query.close();
+ }
+ }
+
+ public void cursorClosed() {
+ mCursor = null;
+ }
+
+ public void setBindArguments(String[] bindArgs) {
+ final int numArgs = bindArgs.length;
+ for (int i = 0; i < numArgs; i++) {
+ mQuery.bindString(i + 1, bindArgs[i]);
+ }
+ }
+
+ public void cursorDeactivated() {
+ // Do nothing
+ }
+
+ public void cursorRequeried(Cursor cursor) {
+ // Do nothing
+ }
+
+ @Override
+ public String toString() {
+ return "SQLiteDirectCursorDriver: " + mSql;
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteDiskIOException.java b/core/java/android/database/sqlite/SQLiteDiskIOException.java
new file mode 100644
index 0000000..01b2069
--- /dev/null
+++ b/core/java/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/core/java/android/database/sqlite/SQLiteDoneException.java b/core/java/android/database/sqlite/SQLiteDoneException.java
new file mode 100644
index 0000000..d6d3f66
--- /dev/null
+++ b/core/java/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/core/java/android/database/sqlite/SQLiteException.java b/core/java/android/database/sqlite/SQLiteException.java
new file mode 100644
index 0000000..3a97bfb
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteException.java
@@ -0,0 +1,30 @@
+/*
+ * 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);
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteFullException.java b/core/java/android/database/sqlite/SQLiteFullException.java
new file mode 100644
index 0000000..582d930
--- /dev/null
+++ b/core/java/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/core/java/android/database/sqlite/SQLiteMisuseException.java b/core/java/android/database/sqlite/SQLiteMisuseException.java
new file mode 100644
index 0000000..685f3ea
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteMisuseException.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 SQLiteMisuseException extends SQLiteException {
+ public SQLiteMisuseException() {}
+
+ public SQLiteMisuseException(String error) {
+ super(error);
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteOpenHelper.java b/core/java/android/database/sqlite/SQLiteOpenHelper.java
new file mode 100644
index 0000000..52aac3a
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteOpenHelper.java
@@ -0,0 +1,229 @@
+/*
+ * 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.content.Context;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+import android.util.Log;
+
+/**
+ * A helper class to manage database creation and version management.
+ * 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>For an example, see the NotePadProvider class in the NotePad sample application,
+ * in the <em>samples/</em> directory of the SDK.</p>
+ */
+public abstract class SQLiteOpenHelper {
+ private static final String TAG = SQLiteOpenHelper.class.getSimpleName();
+
+ private final Context mContext;
+ private final String mName;
+ private final CursorFactory mFactory;
+ private final int mNewVersion;
+
+ private SQLiteDatabase mDatabase = null;
+ private boolean mIsInitializing = false;
+
+ /**
+ * 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.
+ *
+ * @param context to use to open or create 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
+ */
+ public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version) {
+ if (version < 1) throw new IllegalArgumentException("Version must be >= 1, was " + version);
+
+ mContext = context;
+ mName = name;
+ mFactory = factory;
+ mNewVersion = version;
+ }
+
+ /**
+ * Create and/or open a database that will be used for reading and writing.
+ * 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 it.
+ *
+ * <p>Errors such as bad permissions or a full disk may cause this operation
+ * to fail, but future attempts may succeed if the problem is fixed.</p>
+ *
+ * @throws SQLiteException if the database cannot be opened for writing
+ * @return a read/write database object valid until {@link #close} is called
+ */
+ public synchronized SQLiteDatabase getWritableDatabase() {
+ if (mDatabase != null && mDatabase.isOpen() && !mDatabase.isReadOnly()) {
+ return mDatabase; // The database is already open for business
+ }
+
+ if (mIsInitializing) {
+ throw new IllegalStateException("getWritableDatabase called recursively");
+ }
+
+ // If we have a read-only database open, someone could be using it
+ // (though they shouldn't), which would cause a lock to be held on
+ // the file, and our attempts to open the database read-write would
+ // fail waiting for the file lock. To prevent that, we acquire the
+ // lock on the read-only database, which shuts out other users.
+
+ boolean success = false;
+ SQLiteDatabase db = null;
+ if (mDatabase != null) mDatabase.lock();
+ try {
+ mIsInitializing = true;
+ if (mName == null) {
+ db = SQLiteDatabase.create(null);
+ } else {
+ db = mContext.openOrCreateDatabase(mName, 0, mFactory);
+ }
+
+ int version = db.getVersion();
+ if (version != mNewVersion) {
+ db.beginTransaction();
+ try {
+ if (version == 0) {
+ onCreate(db);
+ } else {
+ onUpgrade(db, version, mNewVersion);
+ }
+ db.setVersion(mNewVersion);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ onOpen(db);
+ success = true;
+ return db;
+ } finally {
+ mIsInitializing = false;
+ if (success) {
+ if (mDatabase != null) {
+ try { mDatabase.close(); } catch (Exception e) { }
+ mDatabase.unlock();
+ }
+ mDatabase = db;
+ } else {
+ if (mDatabase != null) mDatabase.unlock();
+ if (db != null) db.close();
+ }
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @throws SQLiteException if the database cannot be opened
+ * @return a database object valid until {@link #getWritableDatabase}
+ * or {@link #close} is called.
+ */
+ public synchronized SQLiteDatabase getReadableDatabase() {
+ if (mDatabase != null && mDatabase.isOpen()) {
+ return mDatabase; // The database is already open for business
+ }
+
+ if (mIsInitializing) {
+ throw new IllegalStateException("getReadableDatabase called recursively");
+ }
+
+ try {
+ return getWritableDatabase();
+ } catch (SQLiteException e) {
+ if (mName == null) throw e; // Can't open a temp database read-only!
+ Log.e(TAG, "Couldn't open " + mName + " for writing (will try read-only):", e);
+ }
+
+ SQLiteDatabase db = null;
+ try {
+ mIsInitializing = true;
+ String path = mContext.getDatabasePath(mName).getPath();
+ db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY);
+ if (db.getVersion() != mNewVersion) {
+ throw new SQLiteException("Can't upgrade read-only database from version " +
+ db.getVersion() + " to " + mNewVersion + ": " + path);
+ }
+
+ onOpen(db);
+ Log.w(TAG, "Opened " + mName + " in read-only mode");
+ mDatabase = db;
+ return mDatabase;
+ } finally {
+ mIsInitializing = false;
+ if (db != null && db != mDatabase) db.close();
+ }
+ }
+
+ /**
+ * 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 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.
+ *
+ * @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 has been opened.
+ * Override method should check {@link SQLiteDatabase#isReadOnly} before
+ * updating the database.
+ *
+ * @param db The database.
+ */
+ public void onOpen(SQLiteDatabase db) {}
+}
diff --git a/core/java/android/database/sqlite/SQLiteProgram.java b/core/java/android/database/sqlite/SQLiteProgram.java
new file mode 100644
index 0000000..f89c87d
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteProgram.java
@@ -0,0 +1,264 @@
+/*
+ * 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.util.Log;
+
+/**
+ * A base class for compiled SQLite programs.
+ */
+public abstract class SQLiteProgram extends SQLiteClosable {
+ static final String TAG = "SQLiteProgram";
+
+ /** The database this program is compiled against. */
+ protected SQLiteDatabase mDatabase;
+
+ /**
+ * Native linkage, do not modify. This comes from the database and should not be modified
+ * in here or in the native code.
+ */
+ protected int nHandle = 0;
+
+ /**
+ * Native linkage, do not modify. When non-0 this holds a reference to a valid
+ * sqlite3_statement object. It is only updated by the native code, but may be
+ * checked in this class when the database lock is held to determine if there
+ * is a valid native-side program or not.
+ */
+ protected int nStatement = 0;
+
+ /**
+ * Used to find out where a cursor was allocated in case it never got
+ * released.
+ */
+ private StackTraceElement[] mStackTraceElements;
+
+ /* package */ SQLiteProgram(SQLiteDatabase db, String sql) {
+ if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
+ mStackTraceElements = new Exception().getStackTrace();
+ }
+
+ mDatabase = db;
+ db.acquireReference();
+ db.addSQLiteClosable(this);
+ this.nHandle = db.mNativeHandle;
+ compile(sql, false);
+ }
+
+ @Override
+ protected void onAllReferencesReleased() {
+ // Note that native_finalize() checks to make sure that nStatement is
+ // non-null before destroying it.
+ native_finalize();
+ mDatabase.releaseReference();
+ mDatabase.removeSQLiteClosable(this);
+ }
+
+ @Override
+ protected void onAllReferencesReleasedFromContainer(){
+ // Note that native_finalize() checks to make sure that nStatement is
+ // non-null before destroying it.
+ native_finalize();
+ mDatabase.releaseReference();
+ }
+
+ /**
+ * Returns a unique identifier for this program.
+ *
+ * @return a unique identifier for this program
+ */
+ public final int getUniqueId() {
+ return nStatement;
+ }
+
+ /**
+ * Compiles the given SQL into a SQLite byte code program using sqlite3_prepare_v2(). If
+ * this method has been called previously without a call to close and forCompilation is set
+ * to false the previous compilation will be used. Setting forceCompilation to true will
+ * always re-compile the program and should be done if you pass differing SQL strings to this
+ * method.
+ *
+ * <P>Note: this method acquires the database lock.</P>
+ *
+ * @param sql the SQL string to compile
+ * @param forceCompilation forces the SQL to be recompiled in the event that there is an
+ * existing compiled SQL program already around
+ */
+ protected void compile(String sql, boolean forceCompilation) {
+ // Only compile if we don't have a valid statement already or the caller has
+ // explicitly requested a recompile.
+ if (nStatement == 0 || forceCompilation) {
+ mDatabase.lock();
+ try {
+ // Note that the native_compile() takes care of destroying any previously
+ // existing programs before it compiles.
+ acquireReference();
+ native_compile(sql);
+ } finally {
+ releaseReference();
+ mDatabase.unlock();
+ }
+ }
+ }
+
+ /**
+ * 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) {
+ acquireReference();
+ try {
+ native_bind_null(index);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /**
+ * Bind a long 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 bindLong(int index, long value) {
+ acquireReference();
+ try {
+ native_bind_long(index, value);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /**
+ * 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) {
+ acquireReference();
+ try {
+ native_bind_double(index, value);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /**
+ * 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
+ */
+ public void bindString(int index, String value) {
+ if (value == null) {
+ throw new IllegalArgumentException("the bind value at index " + index + " is null");
+ }
+ acquireReference();
+ try {
+ native_bind_string(index, value);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /**
+ * 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
+ */
+ public void bindBlob(int index, byte[] value) {
+ if (value == null) {
+ throw new IllegalArgumentException("the bind value at index " + index + " is null");
+ }
+ acquireReference();
+ try {
+ native_bind_blob(index, value);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /**
+ * Clears all existing bindings. Unset bindings are treated as NULL.
+ */
+ public void clearBindings() {
+ acquireReference();
+ try {
+ native_clear_bindings();
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /**
+ * Release this program's resources, making it invalid.
+ */
+ public void close() {
+ mDatabase.lock();
+ try {
+ releaseReference();
+ } finally {
+ mDatabase.unlock();
+ }
+ }
+
+ /**
+ * Make sure that the native resource is cleaned up.
+ */
+ @Override
+ protected void finalize() {
+ if (nStatement != 0) {
+ if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
+ String message = "Finalizing " + this +
+ " that has not been closed";
+
+ Log.d(TAG, message + "\nThis cursor was created in:");
+ for (StackTraceElement ste : mStackTraceElements) {
+ Log.d(TAG, " " + ste);
+ }
+ }
+ // when in finalize() it is already removed from weakhashmap
+ // so it is safe to not removed itself from db
+ onAllReferencesReleasedFromContainer();
+ }
+ }
+
+ /**
+ * Compiles SQL into a SQLite program.
+ *
+ * <P>The database lock must be held when calling this method.
+ * @param sql The SQL to compile.
+ */
+ protected final native void native_compile(String sql);
+ protected final native void native_finalize();
+
+ protected final native void native_bind_null(int index);
+ protected final native void native_bind_long(int index, long value);
+ protected final native void native_bind_double(int index, double value);
+ protected final native void native_bind_string(int index, String value);
+ protected final native void native_bind_blob(int index, byte[] value);
+ private final native void native_clear_bindings();
+}
+
diff --git a/core/java/android/database/sqlite/SQLiteQuery.java b/core/java/android/database/sqlite/SQLiteQuery.java
new file mode 100644
index 0000000..5bfa0e8
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteQuery.java
@@ -0,0 +1,194 @@
+/*
+ * 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.SystemClock;
+
+/**
+ * A SQLite program that represents a query that reads the resulting rows into a CursorWindow.
+ * This class is used by SQLiteCursor and isn't useful itself.
+ */
+public class SQLiteQuery extends SQLiteProgram {
+ //private static final String TAG = "Cursor";
+
+ /** The index of the unbound OFFSET parameter */
+ private int mOffsetIndex;
+
+ /** The SQL used to create this query */
+ private String mQuery;
+
+ /** Args to bind on requery */
+ private String[] mBindArgs;
+
+ private boolean mClosed = false;
+
+ /**
+ * Create a persistent query object.
+ *
+ * @param db The database that this query object is associated with
+ * @param query The SQL string for this query.
+ * @param offsetIndex The 1-based index to the OFFSET parameter,
+ */
+ /* package */ SQLiteQuery(SQLiteDatabase db, String query, int offsetIndex, String[] bindArgs) {
+ super(db, query);
+
+ mOffsetIndex = offsetIndex;
+ mQuery = query;
+ mBindArgs = bindArgs;
+ }
+
+ /**
+ * Reads rows into a buffer. This method acquires the database lock.
+ *
+ * @param window The window to fill into
+ * @return number of total rows in the query
+ */
+ /* package */ int fillWindow(CursorWindow window,
+ int maxRead, int lastPos) {
+ mDatabase.lock();
+
+ boolean logStats = mDatabase.mLogStats;
+ long startTime = logStats ? SystemClock.elapsedRealtime() : 0;
+ try {
+ acquireReference();
+ try {
+ window.acquireReference();
+ // if the start pos is not equal to 0, then most likely window is
+ // too small for the data set, loading by another thread
+ // is not safe in this situation. the native code will ignore maxRead
+ int numRows = native_fill_window(window, window.getStartPosition(), mOffsetIndex,
+ maxRead, lastPos);
+ if (logStats) {
+ mDatabase.logTimeStat(true /* read */, startTime,
+ SystemClock.elapsedRealtime());
+ }
+ return numRows;
+ } catch (IllegalStateException e){
+ // simply ignore it
+ return 0;
+ } catch (SQLiteDatabaseCorruptException e) {
+ mDatabase.onCorruption();
+ throw e;
+ } finally {
+ window.releaseReference();
+ }
+ } finally {
+ releaseReference();
+ mDatabase.unlock();
+ }
+ }
+
+ /**
+ * Get the column count for the statement. Only valid on query based
+ * statements. The database must be locked
+ * when calling this method.
+ *
+ * @return The number of column in the statement's result set.
+ */
+ /* package */ int columnCountLocked() {
+ acquireReference();
+ try {
+ return native_column_count();
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /**
+ * Retrieves the column name for the given column index. The database must be locked
+ * when calling this method.
+ *
+ * @param columnIndex the index of the column to get the name for
+ * @return The requested column's name
+ */
+ /* package */ String columnNameLocked(int columnIndex) {
+ acquireReference();
+ try {
+ return native_column_name(columnIndex);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /** {@hide pending API Council approval} */
+ @Override
+ public String toString() {
+ return "SQLiteQuery: " + mQuery;
+ }
+
+ @Override
+ public void close() {
+ super.close();
+ mClosed = true;
+ }
+
+ /**
+ * Called by SQLiteCursor when it is requeried.
+ */
+ /* package */ void requery() {
+ if (mBindArgs != null) {
+ int len = mBindArgs.length;
+ try {
+ for (int i = 0; i < len; i++) {
+ super.bindString(i + 1, mBindArgs[i]);
+ }
+ } catch (SQLiteMisuseException e) {
+ StringBuilder errMsg = new StringBuilder("mQuery " + mQuery);
+ for (int i = 0; i < len; i++) {
+ errMsg.append(" ");
+ errMsg.append(mBindArgs[i]);
+ }
+ errMsg.append(" ");
+ IllegalStateException leakProgram = new IllegalStateException(
+ errMsg.toString(), e);
+ throw leakProgram;
+ }
+ }
+ }
+
+ @Override
+ public void bindNull(int index) {
+ mBindArgs[index - 1] = null;
+ if (!mClosed) super.bindNull(index);
+ }
+
+ @Override
+ public void bindLong(int index, long value) {
+ mBindArgs[index - 1] = Long.toString(value);
+ if (!mClosed) super.bindLong(index, value);
+ }
+
+ @Override
+ public void bindDouble(int index, double value) {
+ mBindArgs[index - 1] = Double.toString(value);
+ if (!mClosed) super.bindDouble(index, value);
+ }
+
+ @Override
+ public void bindString(int index, String value) {
+ mBindArgs[index - 1] = value;
+ if (!mClosed) super.bindString(index, value);
+ }
+
+ private final native int native_fill_window(CursorWindow window,
+ int startPos, int offsetParam, int maxRead, int lastPos);
+
+ private final native int native_column_count();
+
+ private final native String native_column_name(int columnIndex);
+}
diff --git a/core/java/android/database/sqlite/SQLiteQueryBuilder.java b/core/java/android/database/sqlite/SQLiteQueryBuilder.java
new file mode 100644
index 0000000..519a81c
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteQueryBuilder.java
@@ -0,0 +1,520 @@
+/*
+ * 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.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.provider.BaseColumns;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.Map.Entry;
+
+/**
+ * This is a convience class that helps build SQL queries to be sent to
+ * {@link SQLiteDatabase} objects.
+ */
+public class SQLiteQueryBuilder
+{
+ private static final String TAG = "SQLiteQueryBuilder";
+
+ private Map<String, String> mProjectionMap = null;
+ private String mTables = "";
+ private StringBuilder mWhereClause = new StringBuilder(64);
+ private boolean mDistinct;
+ private SQLiteDatabase.CursorFactory mFactory;
+
+ public SQLiteQueryBuilder() {
+ mDistinct = false;
+ mFactory = null;
+ }
+
+ /**
+ * Mark the query as DISTINCT.
+ *
+ * @param distinct if true the query is DISTINCT, otherwise it isn't
+ */
+ public void setDistinct(boolean distinct) {
+ mDistinct = distinct;
+ }
+
+ /**
+ * Returns the list of tables being queried
+ *
+ * @return the list of tables being queried
+ */
+ public 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(String inTables) {
+ mTables = inTables;
+ }
+
+ /**
+ * Append a chunk to the WHERE clause of the query. All chunks appended are surrounded
+ * by parenthesis and ANDed with the selection passed to {@link #query}. The final
+ * WHERE clause looks like:
+ *
+ * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
+ *
+ * @param inWhere the chunk of text to append to the WHERE clause.
+ */
+ public void appendWhere(CharSequence inWhere) {
+ if (mWhereClause.length() == 0) {
+ mWhereClause.append('(');
+ }
+ mWhereClause.append(inWhere);
+ }
+
+ /**
+ * Append a chunk to the WHERE clause of the query. All chunks appended are surrounded
+ * by parenthesis and ANDed with the selection passed to {@link #query}. The final
+ * WHERE clause looks like:
+ *
+ * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
+ *
+ * @param inWhere the chunk of text to append to the WHERE clause. it will be escaped
+ * to avoid SQL injection attacks
+ */
+ public void appendWhereEscapeString(String inWhere) {
+ if (mWhereClause.length() == 0) {
+ mWhereClause.append('(');
+ }
+ DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere);
+ }
+
+ /**
+ * 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(Map<String, String> columnMap) {
+ mProjectionMap = columnMap;
+ }
+
+ /**
+ * 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 factor to use
+ */
+ public void setCursorFactory(SQLiteDatabase.CursorFactory factory) {
+ mFactory = factory;
+ }
+
+ /**
+ * 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
+ * WHERE clause (excluding the 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
+ * 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 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);
+ appendClauseEscapeClause(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);
+ }
+ }
+
+ private static void appendClauseEscapeClause(StringBuilder s, String name, String clause) {
+ if (!TextUtils.isEmpty(clause)) {
+ s.append(name);
+ DatabaseUtils.appendEscapedSQLString(s, 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 WHERE clause (excluding the 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 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 sortOrder 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 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 */);
+ }
+
+ /**
+ * 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 WHERE clause (excluding the 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 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 sortOrder 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 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) {
+ if (mTables == null) {
+ return null;
+ }
+
+ String sql = buildQuery(
+ projectionIn, selection, selectionArgs, groupBy, having,
+ sortOrder, limit);
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Performing query: " + sql);
+ }
+ return db.rawQueryWithFactory(
+ mFactory, sql, selectionArgs,
+ SQLiteDatabase.findEditTable(mTables));
+ }
+
+ /**
+ * Construct a SELECT statement suitable for use in a group of
+ * SELECT statements that will be joined through 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 WHERE clause (excluding the 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 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 sortOrder 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 the resulting SQL SELECT statement
+ */
+ public String buildQuery(
+ String[] projectionIn, String selection, String[] selectionArgs,
+ String groupBy, String having, String sortOrder, String limit) {
+ String[] projection = computeProjection(projectionIn);
+
+ if (mWhereClause.length() > 0) {
+ mWhereClause.append(')');
+ }
+
+ // Tack on the user's selection, if present.
+ if (selection != null && selection.length() > 0) {
+ if (mWhereClause.length() > 0) {
+ mWhereClause.append(" AND ");
+ }
+
+ mWhereClause.append('(');
+ mWhereClause.append(selection);
+ mWhereClause.append(')');
+ }
+
+ return buildQueryString(
+ mDistinct, mTables, projection, mWhereClause.toString(),
+ groupBy, having, sortOrder, limit);
+ }
+
+ /**
+ * Construct a SELECT statement suitable for use in a group of
+ * SELECT statements that will be joined through 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 SELECT is querying (i.e. mTables), but that do
+ * appear in one of the other tables in the 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 WHERE clause (excluding the 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 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.
+ * @return the resulting SQL SELECT statement
+ */
+ public String buildUnionSubQuery(
+ String typeDiscriminatorColumn,
+ String[] unionColumns,
+ Set<String> columnsPresentInTable,
+ int computedColumnsOffset,
+ String typeDiscriminatorValue,
+ String selection,
+ String[] selectionArgs,
+ 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, selectionArgs, groupBy, having,
+ null /* sortOrder */,
+ null /* limit */);
+ }
+
+ /**
+ * Given a set of subqueries, all of which are SELECT statements,
+ * construct a query that returns the union of what those
+ * subqueries return.
+ * @param subQueries an array of SQL 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
+ * ORDER BY clause (excluding the 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 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 String[] computeProjection(String[] projectionIn) {
+ if (projectionIn != null && projectionIn.length > 0) {
+ if (mProjectionMap != null) {
+ String[] projection = new String[projectionIn.length];
+ int length = projectionIn.length;
+
+ for (int i = 0; i < length; i++) {
+ String userColumn = projectionIn[i];
+ String column = mProjectionMap.get(userColumn);
+
+ if (column == null) {
+ throw new IllegalArgumentException(
+ "Invalid column " + projectionIn[i]);
+ } else {
+ projection[i] = column;
+ }
+ }
+ return projection;
+ } else {
+ return projectionIn;
+ }
+ } 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;
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteStatement.java b/core/java/android/database/sqlite/SQLiteStatement.java
new file mode 100644
index 0000000..5889ad9
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteStatement.java
@@ -0,0 +1,168 @@
+/*
+ * 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.os.SystemClock;
+import android.util.Log;
+
+/**
+ * A pre-compiled statement against a {@link SQLiteDatabase} that can be reused.
+ * The statement cannot return multiple rows, but 1x1 result sets are allowed.
+ * Don't use SQLiteStatement constructor directly, please use
+ * {@link SQLiteDatabase#compileStatement(String)}
+ */
+public class SQLiteStatement extends SQLiteProgram
+{
+ private static final String TAG = "SQLiteStatement";
+
+ private final String mSql;
+
+ /**
+ * Don't use SQLiteStatement constructor directly, please use
+ * {@link SQLiteDatabase#compileStatement(String)}
+ * @param db
+ * @param sql
+ */
+ /* package */ SQLiteStatement(SQLiteDatabase db, String sql) {
+ super(db, sql);
+ if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
+ mSql = sql;
+ } else {
+ mSql = null;
+ }
+ }
+
+ /**
+ * Execute this SQL statement, if it is not a query. For example,
+ * CREATE TABLE, DELTE, INSERT, etc.
+ *
+ * @throws android.database.SQLException If the SQL string is invalid for
+ * some reason
+ */
+ public void execute() {
+ mDatabase.lock();
+ boolean logStats = mDatabase.mLogStats;
+ long startTime = logStats ? SystemClock.elapsedRealtime() : 0;
+
+ acquireReference();
+ try {
+ if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
+ Log.v(TAG, "execute() for [" + mSql + "]");
+ }
+ native_execute();
+ if (logStats) {
+ mDatabase.logTimeStat(false /* write */, startTime, SystemClock.elapsedRealtime());
+ }
+ } finally {
+ releaseReference();
+ mDatabase.unlock();
+ }
+ }
+
+ /**
+ * Execute this SQL statement and return the ID of the most
+ * recently inserted row. The SQL statement should probably be an
+ * INSERT for this to be a useful call.
+ *
+ * @return the row ID of the last row inserted.
+ *
+ * @throws android.database.SQLException If the SQL string is invalid for
+ * some reason
+ */
+ public long executeInsert() {
+ mDatabase.lock();
+ boolean logStats = mDatabase.mLogStats;
+ long startTime = logStats ? SystemClock.elapsedRealtime() : 0;
+
+ acquireReference();
+ try {
+ if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
+ Log.v(TAG, "executeInsert() for [" + mSql + "]");
+ }
+ native_execute();
+ if (logStats) {
+ mDatabase.logTimeStat(false /* write */, startTime, SystemClock.elapsedRealtime());
+ }
+ return mDatabase.lastInsertRow();
+ } finally {
+ releaseReference();
+ mDatabase.unlock();
+ }
+ }
+
+ /**
+ * 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() {
+ mDatabase.lock();
+ boolean logStats = mDatabase.mLogStats;
+ long startTime = logStats ? SystemClock.elapsedRealtime() : 0;
+
+ acquireReference();
+ try {
+ if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
+ Log.v(TAG, "simpleQueryForLong() for [" + mSql + "]");
+ }
+ long retValue = native_1x1_long();
+ if (logStats) {
+ mDatabase.logTimeStat(false /* write */, startTime, SystemClock.elapsedRealtime());
+ }
+ return retValue;
+ } finally {
+ releaseReference();
+ mDatabase.unlock();
+ }
+ }
+
+ /**
+ * 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() {
+ mDatabase.lock();
+ boolean logStats = mDatabase.mLogStats;
+ long startTime = logStats ? SystemClock.elapsedRealtime() : 0;
+
+ acquireReference();
+ try {
+ if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
+ Log.v(TAG, "simpleQueryForString() for [" + mSql + "]");
+ }
+ String retValue = native_1x1_string();
+ if (logStats) {
+ mDatabase.logTimeStat(false /* write */, startTime, SystemClock.elapsedRealtime());
+ }
+ return retValue;
+ } finally {
+ releaseReference();
+ mDatabase.unlock();
+ }
+ }
+
+ private final native void native_execute();
+ private final native long native_1x1_long();
+ private final native String native_1x1_string();
+}
diff --git a/core/java/android/database/sqlite/package.html b/core/java/android/database/sqlite/package.html
new file mode 100644
index 0000000..ff0f9f5
--- /dev/null
+++ b/core/java/android/database/sqlite/package.html
@@ -0,0 +1,20 @@
+<HTML>
+<BODY>
+Contains the SQLite database management
+classes that an application would use to manage its own private database.
+<p>
+Applications use these classes to maange private databases. If creating a
+content provider, you will probably have to use these classes to create and
+manage your own database to store content. See <a
+href="{@docRoot}guide/topics/providers/content-providers.html">Content Providers</a> to learn
+the conventions for implementing a content provider. See the
+NotePadProvider class in the NotePad sample application in the SDK for an
+example of a content provider. Android ships with SQLite version 3.4.0
+<p>If you are working with data sent to you by a provider, you will not use
+these SQLite classes, but instead use the generic {@link android.database}
+classes.
+<p>Android ships with the sqlite3 database tool in the <code>tools/</code>
+folder. You can use this tool to browse or run SQL commands on the device. Run by
+typing <code>sqlite3</code> in a shell window.
+</BODY>
+</HTML>