Home | History | Annotate | Download | only in shadows
      1 package com.xtremelabs.robolectric.shadows;
      2 
      3 import android.database.DataSetObserver;
      4 import android.os.Handler;
      5 import android.view.View;
      6 import android.widget.Adapter;
      7 import android.widget.AdapterView;
      8 import com.xtremelabs.robolectric.internal.Implementation;
      9 import com.xtremelabs.robolectric.internal.Implements;
     10 import com.xtremelabs.robolectric.internal.RealObject;
     11 
     12 import java.util.ArrayList;
     13 import java.util.List;
     14 
     15 import static com.xtremelabs.robolectric.Robolectric.shadowOf;
     16 
     17 @SuppressWarnings({"UnusedDeclaration"})
     18 @Implements(AdapterView.class)
     19 public class ShadowAdapterView extends ShadowViewGroup {
     20     private static int ignoreRowsAtEndOfList = 0;
     21     private static boolean automaticallyUpdateRowViews = true;
     22 
     23     @RealObject
     24     private AdapterView realAdapterView;
     25 
     26     private Adapter adapter;
     27     private View mEmptyView;
     28     private AdapterView.OnItemSelectedListener onItemSelectedListener;
     29     private AdapterView.OnItemClickListener onItemClickListener;
     30     private AdapterView.OnItemLongClickListener onItemLongClickListener;
     31     private boolean valid = false;
     32     private int selectedPosition;
     33     private int itemCount = 0;
     34 
     35     private List<Object> previousItems = new ArrayList<Object>();
     36 
     37     @Implementation
     38     public void setAdapter(Adapter adapter) {
     39         this.adapter = adapter;
     40 
     41         if (null != adapter) {
     42             adapter.registerDataSetObserver(new AdapterViewDataSetObserver());
     43         }
     44 
     45         invalidateAndScheduleUpdate();
     46         setSelection(0);
     47     }
     48 
     49     @Implementation
     50     public void setEmptyView(View emptyView) {
     51         this.mEmptyView = emptyView;
     52         updateEmptyStatus(adapter == null || adapter.isEmpty());
     53     }
     54 
     55     @Implementation
     56     public int getPositionForView(android.view.View view) {
     57         while (view.getParent() != null && view.getParent() != realView) {
     58             view = (View) view.getParent();
     59         }
     60 
     61         for (int i = 0; i < getChildCount(); i++) {
     62             if (view == getChildAt(i)) {
     63                 return i;
     64             }
     65         }
     66 
     67         return AdapterView.INVALID_POSITION;
     68     }
     69 
     70     private void invalidateAndScheduleUpdate() {
     71         valid = false;
     72         itemCount = adapter == null ? 0 : adapter.getCount();
     73         if (mEmptyView != null) {
     74             updateEmptyStatus(itemCount == 0);
     75         }
     76 
     77         if (hasOnItemSelectedListener() && itemCount == 0) {
     78             onItemSelectedListener.onNothingSelected(realAdapterView);
     79         }
     80 
     81         new Handler().post(new Runnable() {
     82             @Override
     83             public void run() {
     84                 if (!valid) {
     85                     update();
     86                     valid = true;
     87                 }
     88             }
     89         });
     90     }
     91 
     92     private boolean hasOnItemSelectedListener() {
     93         return onItemSelectedListener != null;
     94     }
     95 
     96     private void updateEmptyStatus(boolean empty) {
     97         // code taken from the real AdapterView and commented out where not (yet?) applicable
     98 
     99         // we don't deal with filterMode yet...
    100 //        if (isInFilterMode()) {
    101 //            empty = false;
    102 //        }
    103 
    104         if (empty) {
    105             if (mEmptyView != null) {
    106                 mEmptyView.setVisibility(View.VISIBLE);
    107                 setVisibility(View.GONE);
    108             } else {
    109                 // If the caller just removed our empty view, make sure the list view is visible
    110                 setVisibility(View.VISIBLE);
    111             }
    112 
    113             // leave layout for the moment...
    114 //            // We are now GONE, so pending layouts will not be dispatched.
    115 //            // Force one here to make sure that the state of the list matches
    116 //            // the state of the adapter.
    117 //            if (mDataChanged) {
    118 //                this.onLayout(false, mLeft, mTop, mRight, mBottom);
    119 //            }
    120         } else {
    121             if (mEmptyView != null) {
    122                 mEmptyView.setVisibility(View.GONE);
    123             }
    124             setVisibility(View.VISIBLE);
    125         }
    126     }
    127 
    128     /**
    129      * Check if our adapter's items have changed without {@code onChanged()} or {@code onInvalidated()} having been called.
    130      *
    131      * @return true if the object is valid, false if not
    132      * @throws RuntimeException if the items have been changed without notification
    133      */
    134     public boolean checkValidity() {
    135         update();
    136         return valid;
    137     }
    138 
    139     /**
    140      * Set to avoid calling getView() on the last row(s) during validation. Useful if you are using a special
    141      * last row, e.g. one that goes and fetches more list data as soon as it comes into view. This sets a static
    142      * on the class, so be sure to call it again and set it back to 0 at the end of your test.
    143      *
    144      * @param countOfRows The number of rows to ignore at the end of the list.
    145      * @see com.xtremelabs.robolectric.shadows.ShadowAdapterView#checkValidity()
    146      */
    147     public static void ignoreRowsAtEndOfListDuringValidation(int countOfRows) {
    148         ignoreRowsAtEndOfList = countOfRows;
    149     }
    150 
    151     /**
    152      * Use this static method to turn off the feature of this class which calls getView() on all of the
    153      * adapter's rows in setAdapter() and after notifyDataSetChanged() or notifyDataSetInvalidated() is
    154      * called on the adapter. This feature is turned on by default. This sets a static on the class, so
    155      * set it back to true at the end of your test to avoid test pollution.
    156      *
    157      * @param shouldUpdate false to turn off the feature, true to turn it back on
    158      */
    159     public static void automaticallyUpdateRowViews(boolean shouldUpdate) {
    160         automaticallyUpdateRowViews = shouldUpdate;
    161     }
    162 
    163     @Implementation
    164     public int getSelectedItemPosition() {
    165         return selectedPosition;
    166     }
    167 
    168     @Implementation
    169     public Object getSelectedItem() {
    170         int pos = getSelectedItemPosition();
    171         return getItemAtPosition(pos);
    172     }
    173 
    174     @Implementation
    175     public Adapter getAdapter() {
    176         return adapter;
    177     }
    178 
    179     @Implementation
    180     public int getCount() {
    181         return itemCount;
    182     }
    183 
    184     @Implementation
    185     public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener listener) {
    186         this.onItemSelectedListener = listener;
    187     }
    188 
    189     @Implementation
    190     public final AdapterView.OnItemSelectedListener getOnItemSelectedListener() {
    191         return onItemSelectedListener;
    192     }
    193 
    194     @Implementation
    195     public void setOnItemClickListener(AdapterView.OnItemClickListener listener) {
    196         this.onItemClickListener = listener;
    197     }
    198 
    199     @Implementation
    200     public final AdapterView.OnItemClickListener getOnItemClickListener() {
    201         return onItemClickListener;
    202     }
    203 
    204     @Implementation
    205     public void setOnItemLongClickListener(AdapterView.OnItemLongClickListener listener) {
    206         this.onItemLongClickListener = listener;
    207     }
    208 
    209     @Implementation
    210     public AdapterView.OnItemLongClickListener getOnItemLongClickListener() {
    211         return onItemLongClickListener;
    212     }
    213 
    214     @Implementation
    215     public Object getItemAtPosition(int position) {
    216         Adapter adapter = getAdapter();
    217         return (adapter == null || position < 0) ? null : adapter.getItem(position);
    218     }
    219 
    220     @Implementation
    221     public long getItemIdAtPosition(int position) {
    222         Adapter adapter = getAdapter();
    223         return (adapter == null || position < 0) ? AdapterView.INVALID_ROW_ID : adapter.getItemId(position);
    224     }
    225 
    226     @Implementation
    227     public void setSelection(final int position) {
    228         selectedPosition = position;
    229 
    230         if (selectedPosition >= 0) {
    231             new Handler().post(new Runnable() {
    232                 @Override
    233                 public void run() {
    234                     if (hasOnItemSelectedListener()) {
    235                         onItemSelectedListener.onItemSelected(realAdapterView, getChildAt(position), position, getAdapter().getItemId(position));
    236                     }
    237                 }
    238             });
    239         }
    240     }
    241 
    242     @Implementation
    243     public boolean performItemClick(View view, int position, long id) {
    244         if (onItemClickListener != null) {
    245             onItemClickListener.onItemClick(realAdapterView, view, position, id);
    246             return true;
    247         }
    248         return false;
    249     }
    250 
    251     public boolean performItemLongClick(View view, int position, long id) {
    252         if (onItemLongClickListener != null) {
    253             onItemLongClickListener.onItemLongClick(realAdapterView, view, position, id);
    254             return true;
    255         }
    256         return false;
    257     }
    258 
    259     public boolean performItemClick(int position) {
    260         return realAdapterView.performItemClick(realAdapterView.getChildAt(position),
    261                 position, realAdapterView.getItemIdAtPosition(position));
    262     }
    263 
    264     public int findIndexOfItemContainingText(String targetText) {
    265         for (int i = 0; i < realAdapterView.getChildCount(); i++) {
    266             View childView = realAdapterView.getChildAt(i);
    267             String innerText = shadowOf(childView).innerText();
    268             if (innerText.contains(targetText)) {
    269                 return i;
    270             }
    271         }
    272         return -1;
    273     }
    274 
    275     public View findItemContainingText(String targetText) {
    276         int itemIndex = findIndexOfItemContainingText(targetText);
    277         if (itemIndex == -1) {
    278             return null;
    279         }
    280         return realAdapterView.getChildAt(itemIndex);
    281     }
    282 
    283     public void clickFirstItemContainingText(String targetText) {
    284         int itemIndex = findIndexOfItemContainingText(targetText);
    285         if (itemIndex == -1) {
    286             throw new IllegalArgumentException("No item found containing text \"" + targetText + "\"");
    287         }
    288         performItemClick(itemIndex);
    289     }
    290 
    291     @Implementation
    292     public View getEmptyView() {
    293         return mEmptyView;
    294     }
    295 
    296     private void update() {
    297         if (!automaticallyUpdateRowViews) {
    298             return;
    299         }
    300 
    301         super.removeAllViews();
    302         addViews();
    303     }
    304 
    305     protected void addViews() {
    306         Adapter adapter = getAdapter();
    307         if (adapter != null) {
    308             if (valid && (previousItems.size() - ignoreRowsAtEndOfList != adapter.getCount() - ignoreRowsAtEndOfList)) {
    309                 throw new ArrayIndexOutOfBoundsException("view is valid but adapter.getCount() has changed from " + previousItems.size() + " to " + adapter.getCount());
    310             }
    311 
    312             List<Object> newItems = new ArrayList<Object>();
    313             for (int i = 0; i < adapter.getCount() - ignoreRowsAtEndOfList; i++) {
    314                 View view = adapter.getView(i, null, realAdapterView);
    315                 // don't add null views
    316                 if (view != null) {
    317                     addView(view);
    318                 }
    319                 newItems.add(adapter.getItem(i));
    320             }
    321 
    322             if (valid && !newItems.equals(previousItems)) {
    323                 throw new RuntimeException("view is valid but current items <" + newItems + "> don't match previous items <" + previousItems + ">");
    324             }
    325             previousItems = newItems;
    326         }
    327     }
    328 
    329     /**
    330      * Simple default implementation of {@code android.database.DataSetObserver}
    331      */
    332     protected class AdapterViewDataSetObserver extends DataSetObserver {
    333         @Override
    334         public void onChanged() {
    335             invalidateAndScheduleUpdate();
    336         }
    337 
    338         @Override
    339         public void onInvalidated() {
    340             invalidateAndScheduleUpdate();
    341         }
    342     }
    343 }
    344