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 android.widget;
18 
19 import android.annotation.IdRes;
20 import android.annotation.LayoutRes;
21 import android.compat.annotation.UnsupportedAppUsage;
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.net.Uri;
25 import android.os.Build;
26 import android.view.ContextThemeWrapper;
27 import android.view.LayoutInflater;
28 import android.view.View;
29 import android.view.ViewGroup;
30 
31 import java.util.ArrayList;
32 import java.util.List;
33 import java.util.Map;
34 
35 /**
36  * An easy adapter to map static data to views defined in an XML file. You can specify the data
37  * backing the list as an ArrayList of Maps. Each entry in the ArrayList corresponds to one row
38  * in the list. The Maps contain the data for each row. You also specify an XML file that
39  * defines the views used to display the row, and a mapping from keys in the Map to specific
40  * views.
41  *
42  * Binding data to views occurs in two phases. First, if a
43  * {@link android.widget.SimpleAdapter.ViewBinder} is available,
44  * {@link ViewBinder#setViewValue(android.view.View, Object, String)}
45  * is invoked. If the returned value is true, binding has occurred.
46  * If the returned value is false, the following views are then tried in order:
47  * <ul>
48  * <li> A view that implements Checkable (e.g. CheckBox).  The expected bind value is a boolean.
49  * <li> TextView.  The expected bind value is a string and {@link #setViewText(TextView, String)}
50  * is invoked.
51  * <li> ImageView. The expected bind value is a resource id or a string and
52  * {@link #setViewImage(ImageView, int)} or {@link #setViewImage(ImageView, String)} is invoked.
53  * </ul>
54  * If no appropriate binding can be found, an {@link IllegalStateException} is thrown.
55  */
56 public class SimpleAdapter extends BaseAdapter implements Filterable, ThemedSpinnerAdapter {
57     private final LayoutInflater mInflater;
58 
59     private int[] mTo;
60     private String[] mFrom;
61     private ViewBinder mViewBinder;
62 
63     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
64     private List<? extends Map<String, ?>> mData;
65 
66     private int mResource;
67     private int mDropDownResource;
68 
69     /** Layout inflater used for {@link #getDropDownView(int, View, ViewGroup)}. */
70     private LayoutInflater mDropDownInflater;
71 
72     private SimpleFilter mFilter;
73     private ArrayList<Map<String, ?>> mUnfilteredData;
74 
75     /**
76      * Constructor
77      *
78      * @param context The context where the View associated with this SimpleAdapter is running
79      * @param data A List of Maps. Each entry in the List corresponds to one row in the list. The
80      *        Maps contain the data for each row, and should include all the entries specified in
81      *        "from"
82      * @param resource Resource identifier of a view layout that defines the views for this list
83      *        item. The layout file should include at least those named views defined in "to"
84      * @param from A list of column names that will be added to the Map associated with each
85      *        item.
86      * @param to The views that should display column in the "from" parameter. These should all be
87      *        TextViews. The first N views in this list are given the values of the first N columns
88      *        in the from parameter.
89      */
SimpleAdapter(Context context, List<? extends Map<String, ?>> data, @LayoutRes int resource, String[] from, @IdRes int[] to)90     public SimpleAdapter(Context context, List<? extends Map<String, ?>> data,
91             @LayoutRes int resource, String[] from, @IdRes int[] to) {
92         mData = data;
93         mResource = mDropDownResource = resource;
94         mFrom = from;
95         mTo = to;
96         mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
97     }
98 
99     /**
100      * @see android.widget.Adapter#getCount()
101      */
getCount()102     public int getCount() {
103         return mData.size();
104     }
105 
106     /**
107      * @see android.widget.Adapter#getItem(int)
108      */
getItem(int position)109     public Object getItem(int position) {
110         return mData.get(position);
111     }
112 
113     /**
114      * @see android.widget.Adapter#getItemId(int)
115      */
getItemId(int position)116     public long getItemId(int position) {
117         return position;
118     }
119 
120     /**
121      * @see android.widget.Adapter#getView(int, View, ViewGroup)
122      */
getView(int position, View convertView, ViewGroup parent)123     public View getView(int position, View convertView, ViewGroup parent) {
124         return createViewFromResource(mInflater, position, convertView, parent, mResource);
125     }
126 
createViewFromResource(LayoutInflater inflater, int position, View convertView, ViewGroup parent, int resource)127     private View createViewFromResource(LayoutInflater inflater, int position, View convertView,
128             ViewGroup parent, int resource) {
129         View v;
130         if (convertView == null) {
131             v = inflater.inflate(resource, parent, false);
132         } else {
133             v = convertView;
134         }
135 
136         bindView(position, v);
137 
138         return v;
139     }
140 
141     /**
142      * <p>Sets the layout resource to create the drop down views.</p>
143      *
144      * @param resource the layout resource defining the drop down views
145      * @see #getDropDownView(int, android.view.View, android.view.ViewGroup)
146      */
setDropDownViewResource(int resource)147     public void setDropDownViewResource(int resource) {
148         mDropDownResource = resource;
149     }
150 
151     /**
152      * Sets the {@link android.content.res.Resources.Theme} against which drop-down views are
153      * inflated.
154      * <p>
155      * By default, drop-down views are inflated against the theme of the
156      * {@link Context} passed to the adapter's constructor.
157      *
158      * @param theme the theme against which to inflate drop-down views or
159      *              {@code null} to use the theme from the adapter's context
160      * @see #getDropDownView(int, View, ViewGroup)
161      */
162     @Override
setDropDownViewTheme(Resources.Theme theme)163     public void setDropDownViewTheme(Resources.Theme theme) {
164         if (theme == null) {
165             mDropDownInflater = null;
166         } else if (theme == mInflater.getContext().getTheme()) {
167             mDropDownInflater = mInflater;
168         } else {
169             final Context context = new ContextThemeWrapper(mInflater.getContext(), theme);
170             mDropDownInflater = LayoutInflater.from(context);
171         }
172     }
173 
174     @Override
getDropDownViewTheme()175     public Resources.Theme getDropDownViewTheme() {
176         return mDropDownInflater == null ? null : mDropDownInflater.getContext().getTheme();
177     }
178 
179     @Override
getDropDownView(int position, View convertView, ViewGroup parent)180     public View getDropDownView(int position, View convertView, ViewGroup parent) {
181         final LayoutInflater inflater = mDropDownInflater == null ? mInflater : mDropDownInflater;
182         return createViewFromResource(inflater, position, convertView, parent, mDropDownResource);
183     }
184 
bindView(int position, View view)185     private void bindView(int position, View view) {
186         final Map dataSet = mData.get(position);
187         if (dataSet == null) {
188             return;
189         }
190 
191         final ViewBinder binder = mViewBinder;
192         final String[] from = mFrom;
193         final int[] to = mTo;
194         final int count = to.length;
195 
196         for (int i = 0; i < count; i++) {
197             final View v = view.findViewById(to[i]);
198             if (v != null) {
199                 final Object data = dataSet.get(from[i]);
200                 String text = data == null ? "" : data.toString();
201                 if (text == null) {
202                     text = "";
203                 }
204 
205                 boolean bound = false;
206                 if (binder != null) {
207                     bound = binder.setViewValue(v, data, text);
208                 }
209 
210                 if (!bound) {
211                     if (v instanceof Checkable) {
212                         if (data instanceof Boolean) {
213                             ((Checkable) v).setChecked((Boolean) data);
214                         } else if (v instanceof TextView) {
215                             // Note: keep the instanceof TextView check at the bottom of these
216                             // ifs since a lot of views are TextViews (e.g. CheckBoxes).
217                             setViewText((TextView) v, text);
218                         } else {
219                             throw new IllegalStateException(v.getClass().getName() +
220                                     " should be bound to a Boolean, not a " +
221                                     (data == null ? "<unknown type>" : data.getClass()));
222                         }
223                     } else if (v instanceof TextView) {
224                         // Note: keep the instanceof TextView check at the bottom of these
225                         // ifs since a lot of views are TextViews (e.g. CheckBoxes).
226                         setViewText((TextView) v, text);
227                     } else if (v instanceof ImageView) {
228                         if (data instanceof Integer) {
229                             setViewImage((ImageView) v, (Integer) data);
230                         } else {
231                             setViewImage((ImageView) v, text);
232                         }
233                     } else {
234                         throw new IllegalStateException(v.getClass().getName() + " is not a " +
235                                 " view that can be bounds by this SimpleAdapter");
236                     }
237                 }
238             }
239         }
240     }
241 
242     /**
243      * Returns the {@link ViewBinder} used to bind data to views.
244      *
245      * @return a ViewBinder or null if the binder does not exist
246      *
247      * @see #setViewBinder(android.widget.SimpleAdapter.ViewBinder)
248      */
getViewBinder()249     public ViewBinder getViewBinder() {
250         return mViewBinder;
251     }
252 
253     /**
254      * Sets the binder used to bind data to views.
255      *
256      * @param viewBinder the binder used to bind data to views, can be null to
257      *        remove the existing binder
258      *
259      * @see #getViewBinder()
260      */
setViewBinder(ViewBinder viewBinder)261     public void setViewBinder(ViewBinder viewBinder) {
262         mViewBinder = viewBinder;
263     }
264 
265     /**
266      * Called by bindView() to set the image for an ImageView but only if
267      * there is no existing ViewBinder or if the existing ViewBinder cannot
268      * handle binding to an ImageView.
269      *
270      * This method is called instead of {@link #setViewImage(ImageView, String)}
271      * if the supplied data is an int or Integer.
272      *
273      * @param v ImageView to receive an image
274      * @param value the value retrieved from the data set
275      *
276      * @see #setViewImage(ImageView, String)
277      */
setViewImage(ImageView v, int value)278     public void setViewImage(ImageView v, int value) {
279         v.setImageResource(value);
280     }
281 
282     /**
283      * Called by bindView() to set the image for an ImageView but only if
284      * there is no existing ViewBinder or if the existing ViewBinder cannot
285      * handle binding to an ImageView.
286      *
287      * By default, the value will be treated as an image resource. If the
288      * value cannot be used as an image resource, the value is used as an
289      * image Uri.
290      *
291      * This method is called instead of {@link #setViewImage(ImageView, int)}
292      * if the supplied data is not an int or Integer.
293      *
294      * @param v ImageView to receive an image
295      * @param value the value retrieved from the data set
296      *
297      * @see #setViewImage(ImageView, int)
298      */
setViewImage(ImageView v, String value)299     public void setViewImage(ImageView v, String value) {
300         try {
301             v.setImageResource(Integer.parseInt(value));
302         } catch (NumberFormatException nfe) {
303             v.setImageURI(Uri.parse(value));
304         }
305     }
306 
307     /**
308      * Called by bindView() to set the text for a TextView but only if
309      * there is no existing ViewBinder or if the existing ViewBinder cannot
310      * handle binding to a TextView.
311      *
312      * @param v TextView to receive text
313      * @param text the text to be set for the TextView
314      */
setViewText(TextView v, String text)315     public void setViewText(TextView v, String text) {
316         v.setText(text);
317     }
318 
getFilter()319     public Filter getFilter() {
320         if (mFilter == null) {
321             mFilter = new SimpleFilter();
322         }
323         return mFilter;
324     }
325 
326     /**
327      * This class can be used by external clients of SimpleAdapter to bind
328      * values to views.
329      *
330      * You should use this class to bind values to views that are not
331      * directly supported by SimpleAdapter or to change the way binding
332      * occurs for views supported by SimpleAdapter.
333      *
334      * @see SimpleAdapter#setViewImage(ImageView, int)
335      * @see SimpleAdapter#setViewImage(ImageView, String)
336      * @see SimpleAdapter#setViewText(TextView, String)
337      */
338     public static interface ViewBinder {
339         /**
340          * Binds the specified data to the specified view.
341          *
342          * When binding is handled by this ViewBinder, this method must return true.
343          * If this method returns false, SimpleAdapter will attempts to handle
344          * the binding on its own.
345          *
346          * @param view the view to bind the data to
347          * @param data the data to bind to the view
348          * @param textRepresentation a safe String representation of the supplied data:
349          *        it is either the result of data.toString() or an empty String but it
350          *        is never null
351          *
352          * @return true if the data was bound to the view, false otherwise
353          */
setViewValue(View view, Object data, String textRepresentation)354         boolean setViewValue(View view, Object data, String textRepresentation);
355     }
356 
357     /**
358      * <p>An array filters constrains the content of the array adapter with
359      * a prefix. Each item that does not start with the supplied prefix
360      * is removed from the list.</p>
361      */
362     private class SimpleFilter extends Filter {
363 
364         @Override
performFiltering(CharSequence prefix)365         protected FilterResults performFiltering(CharSequence prefix) {
366             FilterResults results = new FilterResults();
367 
368             if (mUnfilteredData == null) {
369                 mUnfilteredData = new ArrayList<Map<String, ?>>(mData);
370             }
371 
372             if (prefix == null || prefix.length() == 0) {
373                 ArrayList<Map<String, ?>> list = mUnfilteredData;
374                 results.values = list;
375                 results.count = list.size();
376             } else {
377                 String prefixString = prefix.toString().toLowerCase();
378 
379                 ArrayList<Map<String, ?>> unfilteredValues = mUnfilteredData;
380                 int count = unfilteredValues.size();
381 
382                 ArrayList<Map<String, ?>> newValues = new ArrayList<Map<String, ?>>(count);
383 
384                 for (int i = 0; i < count; i++) {
385                     Map<String, ?> h = unfilteredValues.get(i);
386                     if (h != null) {
387 
388                         int len = mTo.length;
389 
390                         for (int j=0; j<len; j++) {
391                             String str =  (String)h.get(mFrom[j]);
392 
393                             String[] words = str.split(" ");
394                             int wordCount = words.length;
395 
396                             for (int k = 0; k < wordCount; k++) {
397                                 String word = words[k];
398 
399                                 if (word.toLowerCase().startsWith(prefixString)) {
400                                     newValues.add(h);
401                                     break;
402                                 }
403                             }
404                         }
405                     }
406                 }
407 
408                 results.values = newValues;
409                 results.count = newValues.size();
410             }
411 
412             return results;
413         }
414 
415         @Override
publishResults(CharSequence constraint, FilterResults results)416         protected void publishResults(CharSequence constraint, FilterResults results) {
417             //noinspection unchecked
418             mData = (List<Map<String, ?>>) results.values;
419             if (results.count > 0) {
420                 notifyDataSetChanged();
421             } else {
422                 notifyDataSetInvalidated();
423             }
424         }
425     }
426 }
427