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}