/*
* 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.content;
import android.database.ContentObserver;
import android.database.Cursor;
import android.os.Handler;
import java.util.HashMap;
import java.util.Map;
import java.util.Observable;
/**
* Caches the contents of a cursor into a Map of String->ContentValues and optionally
* keeps the cache fresh by registering for updates on the content backing the cursor. The column of
* the database that is to be used as the key of the map is user-configurable, and the
* ContentValues contains all columns other than the one that is designated the key.
*
* The cursor data is accessed by row key and column name via getValue().
*/
public class ContentQueryMap extends Observable {
private volatile Cursor mCursor;
private String[] mColumnNames;
private int mKeyColumn;
private Handler mHandlerForUpdateNotifications = null;
private boolean mKeepUpdated = false;
private Map mValues = null;
private ContentObserver mContentObserver;
/** Set when a cursor change notification is received and is cleared on a call to requery(). */
private boolean mDirty = false;
/**
* Creates a ContentQueryMap that caches the content backing the cursor
*
* @param cursor the cursor whose contents should be cached
* @param columnNameOfKey the column that is to be used as the key of the values map
* @param keepUpdated true if the cursor's ContentProvider should be monitored for changes and
* the map updated when changes do occur
* @param handlerForUpdateNotifications the Handler that should be used to receive
* notifications of changes (if requested). Normally you pass null here, but if
* you know that the thread that is creating this isn't a thread that can receive
* messages then you can create your own handler and use that here.
*/
public ContentQueryMap(Cursor cursor, String columnNameOfKey, boolean keepUpdated,
Handler handlerForUpdateNotifications) {
mCursor = cursor;
mColumnNames = mCursor.getColumnNames();
mKeyColumn = mCursor.getColumnIndexOrThrow(columnNameOfKey);
mHandlerForUpdateNotifications = handlerForUpdateNotifications;
setKeepUpdated(keepUpdated);
// If we aren't keeping the cache updated with the current state of the cursor's
// ContentProvider then read it once into the cache. Otherwise the cache will be filled
// automatically.
if (!keepUpdated) {
readCursorIntoCache(cursor);
}
}
/**
* Change whether or not the ContentQueryMap will register with the cursor's ContentProvider
* for change notifications. If you use a ContentQueryMap in an activity you should call this
* with false in onPause(), which means you need to call it with true in onResume()
* if want it to be kept updated.
* @param keepUpdated if true the ContentQueryMap should be registered with the cursor's
* ContentProvider, false otherwise
*/
public void setKeepUpdated(boolean keepUpdated) {
if (keepUpdated == mKeepUpdated) return;
mKeepUpdated = keepUpdated;
if (!mKeepUpdated) {
mCursor.unregisterContentObserver(mContentObserver);
mContentObserver = null;
} else {
if (mHandlerForUpdateNotifications == null) {
mHandlerForUpdateNotifications = new Handler();
}
if (mContentObserver == null) {
mContentObserver = new ContentObserver(mHandlerForUpdateNotifications) {
@Override
public void onChange(boolean selfChange) {
// If anyone is listening, we need to do this now to broadcast
// to the observers. Otherwise, we'll just set mDirty and
// let it query lazily when they ask for the values.
if (countObservers() != 0) {
requery();
} else {
mDirty = true;
}
}
};
}
mCursor.registerContentObserver(mContentObserver);
// mark dirty, since it is possible the cursor's backing data had changed before we
// registered for changes
mDirty = true;
}
}
/**
* Access the ContentValues for the row specified by rowName
* @param rowName which row to read
* @return the ContentValues for the row, or null if the row wasn't present in the cursor
*/
public synchronized ContentValues getValues(String rowName) {
if (mDirty) requery();
return mValues.get(rowName);
}
/** Requeries the cursor and reads the contents into the cache */
public void requery() {
final Cursor cursor = mCursor;
if (cursor == null) {
// If mCursor is null then it means there was a requery() in flight
// while another thread called close(), which nulls out mCursor.
// If this happens ignore the requery() since we are closed anyways.
return;
}
mDirty = false;
if (!cursor.requery()) {
// again, don't do anything if the cursor is already closed
return;
}
readCursorIntoCache(cursor);
setChanged();
notifyObservers();
}
private synchronized void readCursorIntoCache(Cursor cursor) {
// Make a new map so old values returned by getRows() are undisturbed.
int capacity = mValues != null ? mValues.size() : 0;
mValues = new HashMap(capacity);
while (cursor.moveToNext()) {
ContentValues values = new ContentValues();
for (int i = 0; i < mColumnNames.length; i++) {
if (i != mKeyColumn) {
values.put(mColumnNames[i], cursor.getString(i));
}
}
mValues.put(cursor.getString(mKeyColumn), values);
}
}
public synchronized Map getRows() {
if (mDirty) requery();
return mValues;
}
public synchronized void close() {
if (mContentObserver != null) {
mCursor.unregisterContentObserver(mContentObserver);
mContentObserver = null;
}
mCursor.close();
mCursor = null;
}
@Override
protected void finalize() throws Throwable {
if (mCursor != null) close();
super.finalize();
}
}