/* * 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 android.util.Log; import java.lang.ref.WeakReference; 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"; /** * @deprecated This is never updated by this class and should not be used */ @Deprecated protected HashMap> mUpdatedRows; protected int mPos; /** * This must be set to the index of the row ID column by any * subclass that wishes to support updates. * * @deprecated This field should not be used. */ @Deprecated protected int mRowIdColumnIndex; /** * If {@link #mRowIdColumnIndex} is not -1 this contains contains the value of * the column at {@link #mRowIdColumnIndex} for the current row this cursor is * pointing at. * * @deprecated This field should not be used. */ @Deprecated protected Long mCurrentRowID; protected boolean mClosed; protected ContentResolver mContentResolver; private Uri mNotifyUri; private final Object mSelfObserverLock = new Object(); private ContentObserver mSelfObserver; private boolean mSelfObserverRegistered; private final DataSetObservable mDataSetObservable = new DataSetObservable(); private final ContentObservable mContentObservable = new ContentObservable(); private Bundle mExtras = Bundle.EMPTY; /* -------------------------------------------------------- */ /* 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); public int getType(int column) { // Reflects the assumption that all commonly used field types (meaning everything // but blobs) are convertible to strings so it should be safe to call // getString to retrieve them. return FIELD_TYPE_STRING; } // TODO implement getBlob in all cursor types public byte[] getBlob(int column) { throw new UnsupportedOperationException("getBlob is not supported"); } /* -------------------------------------------------------- */ /* Methods that may optionally be implemented by subclasses */ /** * If the cursor is backed by a {@link CursorWindow}, returns a pre-filled * window with the contents of the cursor, otherwise null. * * @return The pre-filled window that backs this cursor, or null if none. */ public CursorWindow getWindow() { return null; } public int getColumnCount() { return getColumnNames().length; } public void deactivate() { onDeactivateOrClose(); } /** @hide */ protected void onDeactivateOrClose() { 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(); onDeactivateOrClose(); } /** * This function is called every time the cursor is successfully scrolled * to a new position, giving the subclass a chance to update any state it * may have. If it returns false the move function will also do so and the * cursor will scroll to the beforeFirst position. * * @param oldPosition the position that we're moving from * @param newPosition the position that we're moving to * @return true if the move is successful, false otherwise */ 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(); } else { buffer.sizeCopied = 0; } } /* -------------------------------------------------------- */ /* Implementation */ public AbstractCursor() { mPos = -1; mRowIdColumnIndex = -1; mCurrentRowID = null; mUpdatedRows = new HashMap>(); } 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; } @Override public void fillWindow(int position, CursorWindow window) { DatabaseUtils.cursorFillWindow(this, position, window); } 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 (false) { 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]; } 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, null); 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 Uri getNotificationUri() { return mNotifyUri; } public boolean getWantsAllOnMoveCalls() { return false; } /** * Sets a {@link Bundle} that will be returned by {@link #getExtras()}. null will * be converted into {@link Bundle#EMPTY}. * * @param extras {@link Bundle} to set. * @hide */ public void setExtras(Bundle extras) { mExtras = (extras == null) ? Bundle.EMPTY : extras; } public Bundle getExtras() { return mExtras; } public Bundle respond(Bundle extras) { return Bundle.EMPTY; } /** * @deprecated Always returns false since Cursors do not support updating rows */ @Deprecated protected boolean isFieldUpdated(int columnIndex) { return false; } /** * @deprecated Always returns null since Cursors do not support updating rows */ @Deprecated protected Object getUpdatedField(int columnIndex) { return null; } /** * This function throws CursorIndexOutOfBoundsException if * the cursor position is out of bounds. Subclass implementations of * the get functions should call this before attempting * to retrieve data. * * @throws CursorIndexOutOfBoundsException */ protected void checkPosition() { if (-1 == mPos || getCount() == mPos) { throw new CursorIndexOutOfBoundsException(mPos, getCount()); } } @Override protected void finalize() { if (mSelfObserver != null && mSelfObserverRegistered == true) { mContentResolver.unregisterContentObserver(mSelfObserver); } } /** * Cursors use this class to track changes others make to their URI. */ protected static class SelfContentObserver extends ContentObserver { WeakReference mCursor; public SelfContentObserver(AbstractCursor cursor) { super(null); mCursor = new WeakReference(cursor); } @Override public boolean deliverSelfNotifications() { return false; } @Override public void onChange(boolean selfChange) { AbstractCursor cursor = mCursor.get(); if (cursor != null) { cursor.onChange(false); } } } }