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 }