1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.content; 18 19 import android.database.ContentObserver; 20 import android.database.Cursor; 21 import android.os.Handler; 22 23 import java.util.HashMap; 24 import java.util.Map; 25 import java.util.Observable; 26 27 /** 28 * Caches the contents of a cursor into a Map of String->ContentValues and optionally 29 * keeps the cache fresh by registering for updates on the content backing the cursor. The column of 30 * the database that is to be used as the key of the map is user-configurable, and the 31 * ContentValues contains all columns other than the one that is designated the key. 32 * <p> 33 * The cursor data is accessed by row key and column name via getValue(). 34 */ 35 public class ContentQueryMap extends Observable { 36 private volatile Cursor mCursor; 37 private String[] mColumnNames; 38 private int mKeyColumn; 39 40 private Handler mHandlerForUpdateNotifications = null; 41 private boolean mKeepUpdated = false; 42 43 private Map<String, ContentValues> mValues = null; 44 45 private ContentObserver mContentObserver; 46 47 /** Set when a cursor change notification is received and is cleared on a call to requery(). */ 48 private boolean mDirty = false; 49 50 /** 51 * Creates a ContentQueryMap that caches the content backing the cursor 52 * 53 * @param cursor the cursor whose contents should be cached 54 * @param columnNameOfKey the column that is to be used as the key of the values map 55 * @param keepUpdated true if the cursor's ContentProvider should be monitored for changes and 56 * the map updated when changes do occur 57 * @param handlerForUpdateNotifications the Handler that should be used to receive 58 * notifications of changes (if requested). Normally you pass null here, but if 59 * you know that the thread that is creating this isn't a thread that can receive 60 * messages then you can create your own handler and use that here. 61 */ ContentQueryMap(Cursor cursor, String columnNameOfKey, boolean keepUpdated, Handler handlerForUpdateNotifications)62 public ContentQueryMap(Cursor cursor, String columnNameOfKey, boolean keepUpdated, 63 Handler handlerForUpdateNotifications) { 64 mCursor = cursor; 65 mColumnNames = mCursor.getColumnNames(); 66 mKeyColumn = mCursor.getColumnIndexOrThrow(columnNameOfKey); 67 mHandlerForUpdateNotifications = handlerForUpdateNotifications; 68 setKeepUpdated(keepUpdated); 69 70 // If we aren't keeping the cache updated with the current state of the cursor's 71 // ContentProvider then read it once into the cache. Otherwise the cache will be filled 72 // automatically. 73 if (!keepUpdated) { 74 readCursorIntoCache(cursor); 75 } 76 } 77 78 /** 79 * Change whether or not the ContentQueryMap will register with the cursor's ContentProvider 80 * for change notifications. If you use a ContentQueryMap in an activity you should call this 81 * with false in onPause(), which means you need to call it with true in onResume() 82 * if want it to be kept updated. 83 * @param keepUpdated if true the ContentQueryMap should be registered with the cursor's 84 * ContentProvider, false otherwise 85 */ setKeepUpdated(boolean keepUpdated)86 public void setKeepUpdated(boolean keepUpdated) { 87 if (keepUpdated == mKeepUpdated) return; 88 mKeepUpdated = keepUpdated; 89 90 if (!mKeepUpdated) { 91 mCursor.unregisterContentObserver(mContentObserver); 92 mContentObserver = null; 93 } else { 94 if (mHandlerForUpdateNotifications == null) { 95 mHandlerForUpdateNotifications = new Handler(); 96 } 97 if (mContentObserver == null) { 98 mContentObserver = new ContentObserver(mHandlerForUpdateNotifications) { 99 @Override 100 public void onChange(boolean selfChange) { 101 // If anyone is listening, we need to do this now to broadcast 102 // to the observers. Otherwise, we'll just set mDirty and 103 // let it query lazily when they ask for the values. 104 if (countObservers() != 0) { 105 requery(); 106 } else { 107 mDirty = true; 108 } 109 } 110 }; 111 } 112 mCursor.registerContentObserver(mContentObserver); 113 // mark dirty, since it is possible the cursor's backing data had changed before we 114 // registered for changes 115 mDirty = true; 116 } 117 } 118 119 /** 120 * Access the ContentValues for the row specified by rowName 121 * @param rowName which row to read 122 * @return the ContentValues for the row, or null if the row wasn't present in the cursor 123 */ getValues(String rowName)124 public synchronized ContentValues getValues(String rowName) { 125 if (mDirty) requery(); 126 return mValues.get(rowName); 127 } 128 129 /** Requeries the cursor and reads the contents into the cache */ requery()130 public void requery() { 131 final Cursor cursor = mCursor; 132 if (cursor == null) { 133 // If mCursor is null then it means there was a requery() in flight 134 // while another thread called close(), which nulls out mCursor. 135 // If this happens ignore the requery() since we are closed anyways. 136 return; 137 } 138 mDirty = false; 139 if (!cursor.requery()) { 140 // again, don't do anything if the cursor is already closed 141 return; 142 } 143 readCursorIntoCache(cursor); 144 setChanged(); 145 notifyObservers(); 146 } 147 readCursorIntoCache(Cursor cursor)148 private synchronized void readCursorIntoCache(Cursor cursor) { 149 // Make a new map so old values returned by getRows() are undisturbed. 150 int capacity = mValues != null ? mValues.size() : 0; 151 mValues = new HashMap<String, ContentValues>(capacity); 152 while (cursor.moveToNext()) { 153 ContentValues values = new ContentValues(); 154 for (int i = 0; i < mColumnNames.length; i++) { 155 if (i != mKeyColumn) { 156 values.put(mColumnNames[i], cursor.getString(i)); 157 } 158 } 159 mValues.put(cursor.getString(mKeyColumn), values); 160 } 161 } 162 getRows()163 public synchronized Map<String, ContentValues> getRows() { 164 if (mDirty) requery(); 165 return mValues; 166 } 167 close()168 public synchronized void close() { 169 if (mContentObserver != null) { 170 mCursor.unregisterContentObserver(mContentObserver); 171 mContentObserver = null; 172 } 173 mCursor.close(); 174 mCursor = null; 175 } 176 177 @Override finalize()178 protected void finalize() throws Throwable { 179 if (mCursor != null) close(); 180 super.finalize(); 181 } 182 } 183