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
17package com.xtremelabs.robolectric.shadows;
18
19
20import android.content.Context;
21import android.database.ContentObserver;
22import android.database.Cursor;
23import android.database.DataSetObserver;
24import android.os.Handler;
25import android.util.Config;
26import android.util.Log;
27import android.view.View;
28import android.view.ViewGroup;
29import android.widget.CursorAdapter;
30import android.widget.FilterQueryProvider;
31
32import com.xtremelabs.robolectric.internal.Implementation;
33import com.xtremelabs.robolectric.internal.Implements;
34
35import java.util.ArrayList;
36import 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)
44public class ShadowCursorAdapter extends ShadowBaseAdapter {
45
46    private List<View> views = new ArrayList<View>();
47
48    @Implementation
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     */
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     */
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     */
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
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
163    public Cursor getCursor() {
164        return mCursor;
165    }
166
167    /**
168     * @see android.widget.ListAdapter#getCount()
169     */
170    @Implementation
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
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
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
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
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
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
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
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
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
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 {
415        public ChangeObserver() {
416            super(new Handler());
417        }
418
419        @Override
420        public boolean deliverSelfNotifications() {
421            return true;
422        }
423
424        @Override
425        public void onChange(boolean selfChange) {
426            onContentChangedInternal();
427        }
428    }
429
430    private class MyDataSetObserver extends DataSetObserver {
431        @Override
432        public void onChanged() {
433            mDataValid = true;
434            notifyDataSetChanged();
435        }
436
437        @Override
438        public void onInvalidated() {
439            mDataValid = false;
440            notifyDataSetInvalidated();
441        }
442    }
443
444}