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 import android.content.Context; 20 import android.database.Cursor; 21 import android.net.Uri; 22 import android.view.View; 23 import android.view.ViewGroup; 24 import android.widget.ImageView; 25 import android.widget.SimpleCursorAdapter; 26 import android.widget.SimpleCursorAdapter.CursorToStringConverter; 27 import android.widget.SimpleCursorAdapter.ViewBinder; 28 import android.widget.TextView; 29 30 import com.xtremelabs.robolectric.internal.Implementation; 31 import com.xtremelabs.robolectric.internal.Implements; 32 import com.xtremelabs.robolectric.internal.RealObject; 33 34 /** 35 * An easy adapter to map columns from a cursor to TextViews or ImageViews 36 * defined in an XML file. You can specify which columns you want, which 37 * views you want to display the columns, and the XML file that defines 38 * the appearance of these views. 39 * 40 * Binding occurs in two phases. First, if a 41 * {@link com.xtremelabs.robolectric.shadows.ShadowSimpleCursorAdapter.ViewBinder} is available, 42 * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)} 43 * is invoked. If the returned value is true, binding has occured. If the 44 * returned value is false and the view to bind is a TextView, 45 * {@link #setViewText(TextView, String)} is invoked. If the returned value 46 * is false and the view to bind is an ImageView, 47 * {@link #setViewImage(ImageView, String)} is invoked. If no appropriate 48 * binding can be found, an {@link IllegalStateException} is thrown. 49 * 50 * If this adapter is used with filtering, for instance in an 51 * {@link android.widget.AutoCompleteTextView}, you can use the 52 * {@link com.xtremelabs.robolectric.shadows.ShadowSimpleCursorAdapter.CursorToStringConverter} and the 53 * {@link android.widget.FilterQueryProvider} interfaces 54 * to get control over the filtering process. You can refer to 55 * {@link #convertToString(android.database.Cursor)} and 56 * {@link #runQueryOnBackgroundThread(CharSequence)} for more information. 57 */ 58 @Implements(SimpleCursorAdapter.class) 59 public class ShadowSimpleCursorAdapter extends ShadowResourceCursorAdapter { 60 @RealObject private SimpleCursorAdapter realSimpleCursorAdapter; 61 62 /** 63 * A list of columns containing the data to bind to the UI. 64 * This field should be made private, so it is hidden from the SDK. 65 * {@hide} 66 */ 67 protected int[] mFrom; 68 /** 69 * A list of View ids representing the views to which the data must be bound. 70 * This field should be made private, so it is hidden from the SDK. 71 * {@hide} 72 */ 73 protected int[] mTo; 74 75 private int mStringConversionColumn = -1; 76 private CursorToStringConverter mCursorToStringConverter; 77 private ViewBinder mViewBinder; 78 private String[] mOriginalFrom; 79 80 /** 81 * Constructor. 82 * 83 * @param context The context where the ListView associated with this 84 * SimpleListItemFactory is running 85 * @param layout resource identifier of a layout file that defines the views 86 * for this list item. The layout file should include at least 87 * those named views defined in "to" 88 * @param c The database cursor. Can be null if the cursor is not available yet. 89 * @param from A list of column names representing the data to bind to the UI. Can be null 90 * if the cursor is not available yet. 91 * @param to The views that should display column in the "from" parameter. 92 * These should all be TextViews. The first N views in this list 93 * are given the values of the first N columns in the from 94 * parameter. Can be null if the cursor is not available yet. 95 */ 96 public void __constructor__(Context context, int layout, Cursor c, String[] from, int[] to) { 97 super.__constructor__(context, layout, c); 98 mTo = to; 99 mOriginalFrom = from; 100 findColumns(from); 101 } 102 103 /** 104 * Binds all of the field names passed into the "to" parameter of the 105 * constructor with their corresponding cursor columns as specified in the 106 * "from" parameter. 107 * 108 * Binding occurs in two phases. First, if a 109 * {@link com.xtremelabs.robolectric.shadows.ShadowSimpleCursorAdapter.ViewBinder} is available, 110 * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)} 111 * is invoked. If the returned value is true, binding has occured. If the 112 * returned value is false and the view to bind is a TextView, 113 * {@link #setViewText(TextView, String)} is invoked. If the returned value is 114 * false and the view to bind is an ImageView, 115 * {@link #setViewImage(ImageView, String)} is invoked. If no appropriate 116 * binding can be found, an {@link IllegalStateException} is thrown. 117 * 118 * @throws IllegalStateException if binding cannot occur 119 * 120 * @see android.widget.CursorAdapter#bindView(android.view.View, 121 * android.content.Context, android.database.Cursor) 122 * @see #getViewBinder() 123 * @see #setViewBinder(com.xtremelabs.robolectric.shadows.ShadowSimpleCursorAdapter.ViewBinder) 124 * @see #setViewImage(ImageView, String) 125 * @see #setViewText(TextView, String) 126 */ 127 @Implementation 128 public void bindView(View view, Context context, Cursor cursor) { 129 final ViewBinder binder = mViewBinder; 130 final int count = mTo.length; 131 final int[] from = mFrom; 132 final int[] to = mTo; 133 134 for (int i = 0; i < count; i++) { 135 final View v = view.findViewById(to[i]); 136 if (v != null) { 137 boolean bound = false; 138 if (binder != null) { 139 bound = binder.setViewValue(v, cursor, from[i]); 140 } 141 142 if (!bound) { 143 String text = cursor.getString(from[i]); 144 if (text == null) { 145 text = ""; 146 } 147 148 if (v instanceof TextView) { 149 setViewText((TextView) v, text); 150 } else if (v instanceof ImageView) { 151 setViewImage((ImageView) v, text); 152 } else { 153 throw new IllegalStateException(v.getClass().getName() + " is not a " + 154 " view that can be bounds by this SimpleCursorAdapter"); 155 } 156 } 157 } 158 } 159 } 160 161 /** 162 * Returns the {@link ViewBinder} used to bind data to views. 163 * 164 * @return a ViewBinder or null if the binder does not exist 165 * 166 * @see #bindView(android.view.View, android.content.Context, android.database.Cursor) 167 * @see #setViewBinder(com.xtremelabs.robolectric.shadows.ShadowSimpleCursorAdapter.ViewBinder) 168 */ 169 @Implementation 170 public ViewBinder getViewBinder() { 171 return mViewBinder; 172 } 173 174 /** 175 * Sets the binder used to bind data to views. 176 * 177 * @param viewBinder the binder used to bind data to views, can be null to 178 * remove the existing binder 179 * 180 * @see #bindView(android.view.View, android.content.Context, android.database.Cursor) 181 * @see #getViewBinder() 182 */ 183 @Implementation 184 public void setViewBinder(ViewBinder viewBinder) { 185 mViewBinder = viewBinder; 186 } 187 188 /** 189 * Called by bindView() to set the image for an ImageView but only if 190 * there is no existing ViewBinder or if the existing ViewBinder cannot 191 * handle binding to an ImageView. 192 * 193 * By default, the value will be treated as an image resource. If the 194 * value cannot be used as an image resource, the value is used as an 195 * image Uri. 196 * 197 * Intended to be overridden by Adapters that need to filter strings 198 * retrieved from the database. 199 * 200 * @param v ImageView to receive an image 201 * @param value the value retrieved from the cursor 202 */ 203 @Implementation 204 public void setViewImage(ImageView v, String value) { 205 try { 206 v.setImageResource(Integer.parseInt(value)); 207 } catch (NumberFormatException nfe) { 208 v.setImageURI(Uri.parse(value)); 209 } 210 } 211 212 /** 213 * Called by bindView() to set the text for a TextView but only if 214 * there is no existing ViewBinder or if the existing ViewBinder cannot 215 * handle binding to an TextView. 216 * 217 * Intended to be overridden by Adapters that need to filter strings 218 * retrieved from the database. 219 * 220 * @param v TextView to receive text 221 * @param text the text to be set for the TextView 222 */ 223 @Implementation 224 public void setViewText(TextView v, String text) { 225 v.setText(text); 226 } 227 228 /** 229 * Return the index of the column used to get a String representation 230 * of the Cursor. 231 * 232 * @return a valid index in the current Cursor or -1 233 * 234 * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) 235 * @see #setStringConversionColumn(int) 236 * @see #setCursorToStringConverter(com.xtremelabs.robolectric.shadows.ShadowSimpleCursorAdapter.CursorToStringConverter) 237 * @see #getCursorToStringConverter() 238 */ 239 @Implementation 240 public int getStringConversionColumn() { 241 return mStringConversionColumn; 242 } 243 244 /** 245 * Defines the index of the column in the Cursor used to get a String 246 * representation of that Cursor. The column is used to convert the 247 * Cursor to a String only when the current CursorToStringConverter 248 * is null. 249 * 250 * @param stringConversionColumn a valid index in the current Cursor or -1 to use the default 251 * conversion mechanism 252 * 253 * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) 254 * @see #getStringConversionColumn() 255 * @see #setCursorToStringConverter(com.xtremelabs.robolectric.shadows.ShadowSimpleCursorAdapter.CursorToStringConverter) 256 * @see #getCursorToStringConverter() 257 */ 258 @Implementation 259 public void setStringConversionColumn(int stringConversionColumn) { 260 mStringConversionColumn = stringConversionColumn; 261 } 262 263 /** 264 * Returns the converter used to convert the filtering Cursor 265 * into a String. 266 * 267 * @return null if the converter does not exist or an instance of 268 * {@link com.xtremelabs.robolectric.shadows.ShadowSimpleCursorAdapter.CursorToStringConverter} 269 * 270 * @see #setCursorToStringConverter(com.xtremelabs.robolectric.shadows.ShadowSimpleCursorAdapter.CursorToStringConverter) 271 * @see #getStringConversionColumn() 272 * @see #setStringConversionColumn(int) 273 * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) 274 */ 275 @Implementation 276 public CursorToStringConverter getCursorToStringConverter() { 277 return mCursorToStringConverter; 278 } 279 280 /** 281 * Sets the converter used to convert the filtering Cursor 282 * into a String. 283 * 284 * @param cursorToStringConverter the Cursor to String converter, or 285 * null to remove the converter 286 * 287 * @see #setCursorToStringConverter(com.xtremelabs.robolectric.shadows.ShadowSimpleCursorAdapter.CursorToStringConverter) 288 * @see #getStringConversionColumn() 289 * @see #setStringConversionColumn(int) 290 * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) 291 */ 292 @Implementation 293 public void setCursorToStringConverter(CursorToStringConverter cursorToStringConverter) { 294 mCursorToStringConverter = cursorToStringConverter; 295 } 296 297 /** 298 * Returns a CharSequence representation of the specified Cursor as defined 299 * by the current CursorToStringConverter. If no CursorToStringConverter 300 * has been set, the String conversion column is used instead. If the 301 * conversion column is -1, the returned String is empty if the cursor 302 * is null or Cursor.toString(). 303 * 304 * @param cursor the Cursor to convert to a CharSequence 305 * 306 * @return a non-null CharSequence representing the cursor 307 */ 308 @Implementation 309 public CharSequence convertToString(Cursor cursor) { 310 if (mCursorToStringConverter != null) { 311 return mCursorToStringConverter.convertToString(cursor); 312 } else if (mStringConversionColumn > -1) { 313 return cursor.getString(mStringConversionColumn); 314 } 315 316 return realSimpleCursorAdapter.convertToString(cursor); 317 } 318 319 /** 320 * Create a map from an array of strings to an array of column-id integers in mCursor. 321 * If mCursor is null, the array will be discarded. 322 * 323 * @param from the Strings naming the columns of interest 324 */ 325 private void findColumns(String[] from) { 326 if (mCursor != null) { 327 int i; 328 int count = from.length; 329 if (mFrom == null || mFrom.length != count) { 330 mFrom = new int[count]; 331 } 332 for (i = 0; i < count; i++) { 333 mFrom[i] = mCursor.getColumnIndexOrThrow(from[i]); 334 } 335 } else { 336 mFrom = null; 337 } 338 } 339 340 @Implementation 341 public void changeCursor(Cursor c) { 342 realSimpleCursorAdapter.changeCursor(c); 343 // rescan columns in case cursor layout is different 344 findColumns(mOriginalFrom); 345 } 346 347 /** 348 * Change the cursor and change the column-to-view mappings at the same time. 349 * 350 * @param c The database cursor. Can be null if the cursor is not available yet. 351 * @param from A list of column names representing the data to bind to the UI. Can be null 352 * if the cursor is not available yet. 353 * @param to The views that should display column in the "from" parameter. 354 * These should all be TextViews. The first N views in this list 355 * are given the values of the first N columns in the from 356 * parameter. Can be null if the cursor is not available yet. 357 */ 358 @Implementation 359 public void changeCursorAndColumns(Cursor c, String[] from, int[] to) { 360 mOriginalFrom = from; 361 mTo = to; 362 realSimpleCursorAdapter.changeCursor(c); 363 findColumns(mOriginalFrom); 364 } 365 366 /////////////////////////////////////////////////////////////////////////////////////////////// 367 // Implementation from CursorAdapter 368 369 /** 370 * @see android.widget.ListAdapter#getView(int, View, ViewGroup) 371 */ 372 @Implementation 373 public View getView(int position, View convertView, ViewGroup parent) { 374 if (!mDataValid) { 375 throw new IllegalStateException("this should only be called when the cursor is valid"); 376 } 377 if (!mCursor.moveToPosition(position)) { 378 throw new IllegalStateException("couldn't move cursor to position " + position); 379 } 380 View v; 381 if (convertView == null) { 382 v = newView(mContext, mCursor, parent); 383 } else { 384 v = convertView; 385 } 386 bindView(v, mContext, mCursor); 387 return v; 388 } 389 390 @Implementation 391 public View getDropDownView(int position, View convertView, ViewGroup parent) { 392 if (mDataValid) { 393 mCursor.moveToPosition(position); 394 View v; 395 if (convertView == null) { 396 v = newDropDownView(mContext, mCursor, parent); 397 } else { 398 v = convertView; 399 } 400 bindView(v, mContext, mCursor); 401 return v; 402 } else { 403 return null; 404 } 405 } 406 407 }