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