1 /*
2  * Copyright (C) 2006 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 com.xtremelabs.robolectric.shadows;
18 
19 
20 import android.content.Context;
21 import android.database.ContentObserver;
22 import android.database.Cursor;
23 import android.database.DataSetObserver;
24 import android.os.Handler;
25 import android.util.Config;
26 import android.util.Log;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import android.widget.CursorAdapter;
30 import android.widget.FilterQueryProvider;
31 
32 import com.xtremelabs.robolectric.internal.Implementation;
33 import com.xtremelabs.robolectric.internal.Implements;
34 
35 import java.util.ArrayList;
36 import java.util.List;
37 
38 /**
39  * Adapter that exposes data from a {@link android.database.Cursor Cursor} to a
40  * {@link android.widget.ListView ListView} widget. The Cursor must include
41  * a column named "_id" or this class will not work.
42  */
43 @Implements(CursorAdapter.class)
44 public class ShadowCursorAdapter extends ShadowBaseAdapter {
45 
46     private List<View> views = new ArrayList<View>();
47 
48     @Implementation
getView(int position, View convertView, ViewGroup parent)49     public View getView(int position, View convertView, ViewGroup parent) {
50     	// if the cursor is null OR there are no views to dispense return null
51         if (this.mCursor == null || views.size() == 0 ) {
52             return null;
53         }
54 
55         if (convertView != null) {
56             return convertView;
57         }
58 
59         return views.get(position);
60     }
61 
62     /**
63      * Non-Android API.  Set a list of views to be returned for successive
64      * calls to getView().
65      *
66      * @param views
67      */
setViews(List<View> views)68     public void setViews(List<View> views) {
69         this.views = views;
70     }
71 
72     /**
73      * This field should be made private, so it is hidden from the SDK.
74      * {@hide}
75      */
76     protected boolean mDataValid;
77     /**
78      * This field should be made private, so it is hidden from the SDK.
79      * {@hide}
80      */
81     protected boolean mAutoRequery;
82     /**
83      * This field should be made private, so it is hidden from the SDK.
84      * {@hide}
85      */
86     protected Cursor mCursor;
87     /**
88      * This field should be made private, so it is hidden from the SDK.
89      * {@hide}
90      */
91     protected Context mContext;
92     /**
93      * This field should be made private, so it is hidden from the SDK.
94      * {@hide}
95      */
96     protected int mRowIDColumn;
97     /**
98      * This field should be made private, so it is hidden from the SDK.
99      * {@hide}
100      */
101     protected ChangeObserver mChangeObserver;
102     /**
103      * This field should be made private, so it is hidden from the SDK.
104      * {@hide}
105      */
106     protected DataSetObserver mDataSetObserver = new MyDataSetObserver();
107 //    /**
108 //     * This field should be made private, so it is hidden from the SDK.
109 //     * {@hide}
110 //     */
111 //    protected CursorFilter__FromAndroid mCursorFilter;
112     /**
113      * This field should be made private, so it is hidden from the SDK.
114      * {@hide}
115      */
116     protected FilterQueryProvider mFilterQueryProvider;
117 
118     /**
119      * Constructor. The adapter will call requery() on the cursor whenever
120      * it changes so that the most recent data is always displayed.
121      *
122      * @param c       The cursor from which to get the data.
123      * @param context The context
124      */
__constructor__(Context context, Cursor c)125     public void __constructor__(Context context, Cursor c) {
126         initialize(context, c, true);
127     }
128 
129     /**
130      * Constructor
131      *
132      * @param c           The cursor from which to get the data.
133      * @param context     The context
134      * @param autoRequery If true the adapter will call requery() on the
135      *                    cursor whenever it changes so the most recent
136      *                    data is always displayed.
137      */
__constructor__(Context context, Cursor c, boolean autoRequery)138     public void __constructor__(Context context, Cursor c, boolean autoRequery) {
139         initialize(context, c, autoRequery);
140     }
141 
142     // renamed from Android source so as not to conflict with RobolectricWiringTest
initialize(Context context, Cursor c, boolean autoRequery)143     private void initialize(Context context, Cursor c, boolean autoRequery) {
144         boolean cursorPresent = c != null;
145         mAutoRequery = autoRequery;
146         mCursor = c;
147         mDataValid = cursorPresent;
148         mContext = context;
149         mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1;
150         mChangeObserver = new ChangeObserver();
151         if (cursorPresent) {
152             c.registerContentObserver(mChangeObserver);
153             c.registerDataSetObserver(mDataSetObserver);
154         }
155     }
156 
157     /**
158      * Returns the cursor.
159      *
160      * @return the cursor.
161      */
162     @Implementation
getCursor()163     public Cursor getCursor() {
164         return mCursor;
165     }
166 
167     /**
168      * @see android.widget.ListAdapter#getCount()
169      */
170     @Implementation
getCount()171     public int getCount() {
172         if (mDataValid && mCursor != null) {
173             return mCursor.getCount();
174         } else {
175             return 0;
176         }
177     }
178 
179     /**
180      * @see android.widget.ListAdapter#getItem(int)
181      */
182     @Implementation
getItem(int position)183     public Object getItem(int position) {
184         if (mDataValid && mCursor != null) {
185             mCursor.moveToPosition(position);
186             return mCursor;
187         } else {
188             return null;
189         }
190     }
191 
192     /**
193      * @see android.widget.ListAdapter#getItemId(int)
194      */
195     @Implementation
getItemId(int position)196     public long getItemId(int position) {
197         if (mDataValid && mCursor != null) {
198             this.mCursor.getColumnIndexOrThrow("_id");
199             if (mCursor.moveToPosition(position)) {
200                 return mCursor.getLong(mRowIDColumn);
201             } else {
202                 return 0;
203             }
204         } else {
205             return 0;
206         }
207     }
208 
209     @Implementation
hasStableIds()210     public boolean hasStableIds() {
211         return true;
212     }
213 
214 //    /**
215 //     * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
216 //     */
217 //    @Implementation
218 //    public View getView(int position, View convertView, ViewGroup parent) {
219 //        if (!mDataValid) {
220 //            throw new IllegalStateException("this should only be called when the cursor is valid");
221 //        }
222 //        if (!mCursor.moveToPosition(position)) {
223 //            throw new IllegalStateException("couldn't move cursor to position " + position);
224 //        }
225 //        View v;
226 //        if (convertView == null) {
227 //            v = newView(mContext, mCursor, parent);
228 //        } else {
229 //            v = convertView;
230 //        }
231 //        bindView(v, mContext, mCursor);
232 //        return v;
233 //    }
234 //
235 //    @Implementation
236 //    public View getDropDownView(int position, View convertView, ViewGroup parent) {
237 //        if (mDataValid) {
238 //            mCursor.moveToPosition(position);
239 //            View v;
240 //            if (convertView == null) {
241 //                v = newDropDownView(mContext, mCursor, parent);
242 //            } else {
243 //                v = convertView;
244 //            }
245 //            bindView(v, mContext, mCursor);
246 //            return v;
247 //        } else {
248 //            return null;
249 //        }
250 //    }
251 
252 //    /**
253 //     * Makes a new view to hold the data pointed to by cursor.
254 //     * @param context Interface to application's global information
255 //     * @param cursor The cursor from which to get the data. The cursor is already
256 //     * moved to the correct position.
257 //     * @param parent The parent to which the new view is attached to
258 //     * @return the newly created view.
259 //     */
260 //    public abstract View newView(Context context, Cursor cursor, ViewGroup parent);
261 
262 //    /**
263 //     * Makes a new drop down view to hold the data pointed to by cursor.
264 //     * @param context Interface to application's global information
265 //     * @param cursor The cursor from which to get the data. The cursor is already
266 //     * moved to the correct position.
267 //     * @param parent The parent to which the new view is attached to
268 //     * @return the newly created view.
269 //     */
270 //    @Implementation
271 //    public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) {
272 //        return newView(context, cursor, parent);
273 //    }
274 
275 //    /**
276 //     * Bind an existing view to the data pointed to by cursor
277 //     * @param view Existing view, returned earlier by newView
278 //     * @param context Interface to application's global information
279 //     * @param cursor The cursor from which to get the data. The cursor is already
280 //     * moved to the correct position.
281 //     */
282 //    public abstract void bindView(View view, Context context, Cursor cursor);
283 
284     /**
285      * Change the underlying cursor to a new cursor. If there is an existing cursor it will be
286      * closed.
287      *
288      * @param cursor the new cursor to be used
289      */
290     @Implementation
changeCursor(Cursor cursor)291     public void changeCursor(Cursor cursor) {
292         if (cursor == mCursor) {
293             return;
294         }
295         if (mCursor != null) {
296             mCursor.unregisterContentObserver(mChangeObserver);
297             mCursor.unregisterDataSetObserver(mDataSetObserver);
298             mCursor.close();
299         }
300         mCursor = cursor;
301         if (cursor != null) {
302             cursor.registerContentObserver(mChangeObserver);
303             cursor.registerDataSetObserver(mDataSetObserver);
304             mRowIDColumn = cursor.getColumnIndexOrThrow("_id");
305             mDataValid = true;
306             // notify the observers about the new cursor
307             notifyDataSetChanged();
308         } else {
309             mRowIDColumn = -1;
310             mDataValid = false;
311             // notify the observers about the lack of a data set
312             notifyDataSetInvalidated();
313         }
314     }
315 
316     /**
317      * <p>Converts the cursor into a CharSequence. Subclasses should override this
318      * method to convert their results. The default implementation returns an
319      * empty String for null values or the default String representation of
320      * the value.</p>
321      *
322      * @param cursor the cursor to convert to a CharSequence
323      * @return a CharSequence representing the value
324      */
325     @Implementation
convertToString(Cursor cursor)326     public CharSequence convertToString(Cursor cursor) {
327         return cursor == null ? "" : cursor.toString();
328     }
329 
330     /**
331      * Runs a query with the specified constraint. This query is requested
332      * by the filter attached to this adapter.
333      * <p/>
334      * The query is provided by a
335      * {@link android.widget.FilterQueryProvider}.
336      * If no provider is specified, the current cursor is not filtered and returned.
337      * <p/>
338      * After this method returns the resulting cursor is passed to {@link #changeCursor(Cursor)}
339      * and the previous cursor is closed.
340      * <p/>
341      * This method is always executed on a background thread, not on the
342      * application's main thread (or UI thread.)
343      * <p/>
344      * Contract: when constraint is null or empty, the original results,
345      * prior to any filtering, must be returned.
346      *
347      * @param constraint the constraint with which the query must be filtered
348      * @return a Cursor representing the results of the new query
349      * @see #getFilter()
350      * @see #getFilterQueryProvider()
351      * @see #setFilterQueryProvider(android.widget.FilterQueryProvider)
352      */
353     @Implementation
runQueryOnBackgroundThread(CharSequence constraint)354     public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
355         if (mFilterQueryProvider != null) {
356             return mFilterQueryProvider.runQuery(constraint);
357         }
358 
359         return mCursor;
360     }
361 
362 //    @Implementation
363 //    public Filter getFilter() {
364 //        if (mCursorFilter == null) {
365 //            mCursorFilter = new CursorFilter__FromAndroid(this);
366 //        }
367 //        return mCursorFilter;
368 //    }
369 
370     /**
371      * Returns the query filter provider used for filtering. When the
372      * provider is null, no filtering occurs.
373      *
374      * @return the current filter query provider or null if it does not exist
375      * @see #setFilterQueryProvider(android.widget.FilterQueryProvider)
376      * @see #runQueryOnBackgroundThread(CharSequence)
377      */
378     @Implementation
getFilterQueryProvider()379     public FilterQueryProvider getFilterQueryProvider() {
380         return mFilterQueryProvider;
381     }
382 
383     /**
384      * Sets the query filter provider used to filter the current Cursor.
385      * The provider's
386      * {@link android.widget.FilterQueryProvider#runQuery(CharSequence)}
387      * method is invoked when filtering is requested by a client of
388      * this adapter.
389      *
390      * @param filterQueryProvider the filter query provider or null to remove it
391      * @see #getFilterQueryProvider()
392      * @see #runQueryOnBackgroundThread(CharSequence)
393      */
394     @Implementation
setFilterQueryProvider(FilterQueryProvider filterQueryProvider)395     public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) {
396         mFilterQueryProvider = filterQueryProvider;
397     }
398 
399     /**
400      * Called when the {@link ContentObserver} on the cursor receives a change notification.
401      * The default implementation provides the auto-requery logic, but may be overridden by
402      * sub classes.
403      *
404      * @see ContentObserver#onChange(boolean)
405      */
406     // renamed from Android source so as not to conflict with RobolectricWiringTest
onContentChangedInternal()407     protected void onContentChangedInternal() {
408         if (mAutoRequery && mCursor != null && !mCursor.isClosed()) {
409             if (Config.LOGV) Log.v("Cursor", "Auto requerying " + mCursor + " due to update");
410             mDataValid = mCursor.requery();
411         }
412     }
413 
414     private class ChangeObserver extends ContentObserver {
ChangeObserver()415         public ChangeObserver() {
416             super(new Handler());
417         }
418 
419         @Override
deliverSelfNotifications()420         public boolean deliverSelfNotifications() {
421             return true;
422         }
423 
424         @Override
onChange(boolean selfChange)425         public void onChange(boolean selfChange) {
426             onContentChangedInternal();
427         }
428     }
429 
430     private class MyDataSetObserver extends DataSetObserver {
431         @Override
onChanged()432         public void onChanged() {
433             mDataValid = true;
434             notifyDataSetChanged();
435         }
436 
437         @Override
onInvalidated()438         public void onInvalidated() {
439             mDataValid = false;
440             notifyDataSetInvalidated();
441         }
442     }
443 
444 }