diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
commit | 54b6cfa9a9e5b861a9930af873580d6dc20f773c (patch) | |
tree | 35051494d2af230dce54d6b31c6af8fc24091316 /core/java/android/database | |
download | frameworks_base-54b6cfa9a9e5b861a9930af873580d6dc20f773c.zip frameworks_base-54b6cfa9a9e5b861a9930af873580d6dc20f773c.tar.gz frameworks_base-54b6cfa9a9e5b861a9930af873580d6dc20f773c.tar.bz2 |
Initial Contribution
Diffstat (limited to 'core/java/android/database')
45 files changed, 9415 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..e81f7f8 --- /dev/null +++ b/core/java/android/database/AbstractCursor.java @@ -0,0 +1,617 @@ +/* + * 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 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); + } + } + + 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..1ec4312 --- /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("This cursor has changed, you must call requery()"); + } + } + + @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 <= position <= count. + * + * <p>This method will return true if the request destination was reachable, + * otherwise, it returns false. + * + * @param position the zero-based position to move to. + * @return whether the requested move fully succeeded. + */ + boolean moveToPosition(int position); + + /** + * Move the cursor to the first row. + * + * <p>This method will return false if the cursor is empty. + * + * @return whether the move succeeded. + */ + boolean moveToFirst(); + + /** + * Move the cursor to the last row. + * + * <p>This method will return false if the cursor is empty. + * + * @return whether the move succeeded. + */ + boolean moveToLast(); + + /** + * Move the cursor to the next row. + * + * <p>This method will return false if the cursor is already past the + * last entry in the result set. + * + * @return whether the move succeeded. + */ + boolean moveToNext(); + + /** + * Move the cursor to the previous row. + * + * <p>This method will return false if the cursor is already before the + * first entry in the result set. + * + * @return whether the move succeeded. + */ + boolean moveToPrevious(); + + /** + * Returns whether the cursor is pointing to the first row. + * + * @return whether the cursor is pointing at the first entry. + */ + boolean isFirst(); + + /** + * Returns whether the cursor is pointing to the last row. + * + * @return whether the cursor is pointing at the last entry. + */ + boolean isLast(); + + /** + * Returns whether the cursor is pointing to the position before the first + * row. + * + * @return whether the cursor is before the first result. + */ + boolean isBeforeFirst(); + + /** + * Returns whether the cursor is pointing to the position after the last + * row. + * + * @return whether the cursor is after the last result. + */ + boolean isAfterLast(); + + /** + * 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..72dc3a9 --- /dev/null +++ b/core/java/android/database/CursorWindow.java @@ -0,0 +1,478 @@ +/* + * 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() { + mStartPos = 0; + native_clear(); + } + + /** 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..ab0dc3f --- /dev/null +++ b/core/java/android/database/DatabaseUtils.java @@ -0,0 +1,1002 @@ +/* + * 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); + 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()); + } + } + + /** + * 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/<package_name>/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..ae2fc95 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteCursor.java @@ -0,0 +1,450 @@ +/* + * 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.SQLException; +import android.text.TextUtils; +import android.util.Config; +import android.util.Log; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * 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; + + /** + * 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 { + mWindow.clear(); + } + + // mWindow must be cleared + mCount = mQuery.fillWindow(mWindow, startPos); + } + + @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); + 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() { + 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; + // Requery the program that runs over the temp table + mQuery.requery(); + } 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) { + mWindow.close(); + 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..e497190 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -0,0 +1,1512 @@ +/* + * 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 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 final static String TAG = "Database"; + + /** + * 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; + /** + * @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 { + execSQL("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 + */ + public boolean yieldIfContended() { + 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(); + 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 maximum size the database may grow to. + * + * @return the new maximum database size + */ + 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>")); + } + } + } + + /** + * 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 insertOrReplace(table, nullColumnHack, values, false); + } 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 insertOrReplace(table, nullColumnHack, values, false) ; + } + + /** + * 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 insertOrReplace(table, nullColumnHack, initialValues, true); + } 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 insertOrReplace(table, nullColumnHack, initialValues, true); + } + + private long insertOrReplace(String table, String nullColumnHack, + ContentValues initialValues, boolean allowReplace) { + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } + + // Measurements show most sql lengths <= 152 + StringBuilder sql = new StringBuilder(152); + sql.append("INSERT "); + if (allowReplace) { + sql.append("OR REPLACE "); + } + 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) { + 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 "); + 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 { + long timeStart = 0; + if (Config.LOGV) { + timeStart = System.currentTimeMillis(); + } + lock(); + try { + native_execSQL(sql); + } catch (SQLiteDatabaseCorruptException e) { + onCorruption(); + throw e; + } finally { + unlock(); + } + if (Config.LOGV) { + long timeEnd = System.currentTimeMillis(); + Log.v(TAG, "Executed (" + (timeEnd - timeStart) + " ms):" + sql); + } + } + + /** + * Execute a single SQL statement that is not a query. For example, CREATE + * TABLE, DELETE, INSERT, etc. Multiple statements separated by ;s are not + * supported. it takes a write lock, + * + * @param sql + * @param bindArgs only byte[], String, Long and Double are supported in bindArgs. + * @throws SQLException If the SQL string is invalid for some reason + */ + public void execSQL(String sql, Object[] bindArgs) throws SQLException { + if (bindArgs == null) { + throw new IllegalArgumentException("Empty bindArgs"); + } + long timeStart = 0; + if (Config.LOGV) { + timeStart = System.currentTimeMillis(); + } + 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 (Config.LOGV) { + long timeEnd = System.currentTimeMillis(); + Log.v(TAG, "Executed (" + (timeEnd - timeStart) + " ms):" + sql); + } + } + + @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 createDatabase} 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; + 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; + } + + /** + * 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 mLock} 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 mLock} 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..f6872ac --- /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. + * + * @see com.google.provider.NotePad.NotePadProvider + */ +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..e0341a2 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteProgram.java @@ -0,0 +1,262 @@ +/* + * 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); + } + } + onAllReferencesReleased(); + } + } + + /** + * 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..40855b6 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteQuery.java @@ -0,0 +1,182 @@ +/* + * 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; + +/** + * 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. It must include "INDEX -1 + * OFFSET ?" at the end + * @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 + * @param startPos The position to start reading rows from + * @return number of total rows in the query + */ + /* package */ int fillWindow(CursorWindow window, int startPos) { + if (startPos < 0) { + throw new IllegalArgumentException("startPos should > 0"); + } + window.setStartPosition(startPos); + mDatabase.lock(); + try { + acquireReference(); + window.acquireReference(); + return native_fill_window(window, startPos, mOffsetIndex); + } catch (IllegalStateException e){ + // simply ignore it + return 0; + } catch (SQLiteDatabaseCorruptException e) { + mDatabase.onCorruption(); + throw e; + } finally { + window.releaseReference(); + 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(); + } + } + + @Override + public void close() { + super.close(); + mClosed = true; + } + + /** + * Called by SQLiteCursor when it is requeried. + */ + /* package */ void requery() { + boolean oldMClosed = mClosed; + if (mClosed) { + mClosed = false; + compile(mQuery, false); + } + 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 + ("old mClosed " + oldMClosed + " 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); + + 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 (<append chunk 1><append chunk2>) AND (<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 (<append chunk 1><append chunk2>) AND (<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..bf9361d --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteStatement.java @@ -0,0 +1,118 @@ +/* + * 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; + +/** + * 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 +{ + /** + * 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); + } + + /** + * 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(); + acquireReference(); + try { + native_execute(); + } 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(); + acquireReference(); + try { + native_execute(); + 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(); + acquireReference(); + try { + return native_1x1_long(); + } 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(); + acquireReference(); + try { + return native_1x1_string(); + } 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..c03a8dc --- /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}devel/data.html">Storing, Retrieving and Exposing Data</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> |