1 /*
2  * Copyright (C) 2007 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.preference;
18 
19 import java.util.ArrayList;
20 import java.util.Collections;
21 import java.util.List;
22 
23 import android.graphics.drawable.Drawable;
24 import android.os.Handler;
25 import android.preference.Preference.OnPreferenceChangeInternalListener;
26 import android.view.View;
27 import android.view.ViewGroup;
28 import android.widget.Adapter;
29 import android.widget.BaseAdapter;
30 import android.widget.FrameLayout;
31 import android.widget.ListView;
32 
33 /**
34  * An adapter that returns the {@link Preference} contained in this group.
35  * In most cases, this adapter should be the base class for any custom
36  * adapters from {@link Preference#getAdapter()}.
37  * <p>
38  * This adapter obeys the
39  * {@link Preference}'s adapter rule (the
40  * {@link Adapter#getView(int, View, ViewGroup)} should be used instead of
41  * {@link Preference#getView(ViewGroup)} if a {@link Preference} has an
42  * adapter via {@link Preference#getAdapter()}).
43  * <p>
44  * This adapter also propagates data change/invalidated notifications upward.
45  * <p>
46  * This adapter does not include this {@link PreferenceGroup} in the returned
47  * adapter, use {@link PreferenceCategoryAdapter} instead.
48  *
49  * @see PreferenceCategoryAdapter
50  *
51  * @hide
52  */
53 public class PreferenceGroupAdapter extends BaseAdapter
54         implements OnPreferenceChangeInternalListener {
55 
56     private static final String TAG = "PreferenceGroupAdapter";
57 
58     /**
59      * The group that we are providing data from.
60      */
61     private PreferenceGroup mPreferenceGroup;
62 
63     /**
64      * Maps a position into this adapter -> {@link Preference}. These
65      * {@link Preference}s don't have to be direct children of this
66      * {@link PreferenceGroup}, they can be grand children or younger)
67      */
68     private List<Preference> mPreferenceList;
69 
70     /**
71      * List of unique Preference and its subclasses' names. This is used to find
72      * out how many types of views this adapter can return. Once the count is
73      * returned, this cannot be modified (since the ListView only checks the
74      * count once--when the adapter is being set). We will not recycle views for
75      * Preference subclasses seen after the count has been returned.
76      */
77     private ArrayList<PreferenceLayout> mPreferenceLayouts;
78 
79     private PreferenceLayout mTempPreferenceLayout = new PreferenceLayout();
80 
81     /**
82      * Blocks the mPreferenceClassNames from being changed anymore.
83      */
84     private boolean mHasReturnedViewTypeCount = false;
85 
86     private volatile boolean mIsSyncing = false;
87 
88     private Handler mHandler = new Handler();
89 
90     private Runnable mSyncRunnable = new Runnable() {
91         public void run() {
92             syncMyPreferences();
93         }
94     };
95 
96     private int mHighlightedPosition = -1;
97     private Drawable mHighlightedDrawable;
98 
99     private static ViewGroup.LayoutParams sWrapperLayoutParams = new ViewGroup.LayoutParams(
100             ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
101 
102     private static class PreferenceLayout implements Comparable<PreferenceLayout> {
103         private int resId;
104         private int widgetResId;
105         private String name;
106 
compareTo(PreferenceLayout other)107         public int compareTo(PreferenceLayout other) {
108             int compareNames = name.compareTo(other.name);
109             if (compareNames == 0) {
110                 if (resId == other.resId) {
111                     if (widgetResId == other.widgetResId) {
112                         return 0;
113                     } else {
114                         return widgetResId - other.widgetResId;
115                     }
116                 } else {
117                     return resId - other.resId;
118                 }
119             } else {
120                 return compareNames;
121             }
122         }
123     }
124 
PreferenceGroupAdapter(PreferenceGroup preferenceGroup)125     public PreferenceGroupAdapter(PreferenceGroup preferenceGroup) {
126         mPreferenceGroup = preferenceGroup;
127         // If this group gets or loses any children, let us know
128         mPreferenceGroup.setOnPreferenceChangeInternalListener(this);
129 
130         mPreferenceList = new ArrayList<Preference>();
131         mPreferenceLayouts = new ArrayList<PreferenceLayout>();
132 
133         syncMyPreferences();
134     }
135 
syncMyPreferences()136     private void syncMyPreferences() {
137         synchronized(this) {
138             if (mIsSyncing) {
139                 return;
140             }
141 
142             mIsSyncing = true;
143         }
144 
145         List<Preference> newPreferenceList = new ArrayList<Preference>(mPreferenceList.size());
146         flattenPreferenceGroup(newPreferenceList, mPreferenceGroup);
147         mPreferenceList = newPreferenceList;
148 
149         notifyDataSetChanged();
150 
151         synchronized(this) {
152             mIsSyncing = false;
153             notifyAll();
154         }
155     }
156 
flattenPreferenceGroup(List<Preference> preferences, PreferenceGroup group)157     private void flattenPreferenceGroup(List<Preference> preferences, PreferenceGroup group) {
158         // TODO: shouldn't always?
159         group.sortPreferences();
160 
161         final int groupSize = group.getPreferenceCount();
162         for (int i = 0; i < groupSize; i++) {
163             final Preference preference = group.getPreference(i);
164 
165             preferences.add(preference);
166 
167             if (!mHasReturnedViewTypeCount && preference.canRecycleLayout()) {
168                 addPreferenceClassName(preference);
169             }
170 
171             if (preference instanceof PreferenceGroup) {
172                 final PreferenceGroup preferenceAsGroup = (PreferenceGroup) preference;
173                 if (preferenceAsGroup.isOnSameScreenAsChildren()) {
174                     flattenPreferenceGroup(preferences, preferenceAsGroup);
175                 }
176             }
177 
178             preference.setOnPreferenceChangeInternalListener(this);
179         }
180     }
181 
182     /**
183      * Creates a string that includes the preference name, layout id and widget layout id.
184      * If a particular preference type uses 2 different resources, they will be treated as
185      * different view types.
186      */
createPreferenceLayout(Preference preference, PreferenceLayout in)187     private PreferenceLayout createPreferenceLayout(Preference preference, PreferenceLayout in) {
188         PreferenceLayout pl = in != null? in : new PreferenceLayout();
189         pl.name = preference.getClass().getName();
190         pl.resId = preference.getLayoutResource();
191         pl.widgetResId = preference.getWidgetLayoutResource();
192         return pl;
193     }
194 
addPreferenceClassName(Preference preference)195     private void addPreferenceClassName(Preference preference) {
196         final PreferenceLayout pl = createPreferenceLayout(preference, null);
197         int insertPos = Collections.binarySearch(mPreferenceLayouts, pl);
198 
199         // Only insert if it doesn't exist (when it is negative).
200         if (insertPos < 0) {
201             // Convert to insert index
202             insertPos = insertPos * -1 - 1;
203             mPreferenceLayouts.add(insertPos, pl);
204         }
205     }
206 
getCount()207     public int getCount() {
208         return mPreferenceList.size();
209     }
210 
getItem(int position)211     public Preference getItem(int position) {
212         if (position < 0 || position >= getCount()) return null;
213         return mPreferenceList.get(position);
214     }
215 
getItemId(int position)216     public long getItemId(int position) {
217         if (position < 0 || position >= getCount()) return ListView.INVALID_ROW_ID;
218         return this.getItem(position).getId();
219     }
220 
221     /**
222      * @hide
223      */
setHighlighted(int position)224     public void setHighlighted(int position) {
225         mHighlightedPosition = position;
226     }
227 
228     /**
229      * @hide
230      */
setHighlightedDrawable(Drawable drawable)231     public void setHighlightedDrawable(Drawable drawable) {
232         mHighlightedDrawable = drawable;
233     }
234 
getView(int position, View convertView, ViewGroup parent)235     public View getView(int position, View convertView, ViewGroup parent) {
236         final Preference preference = this.getItem(position);
237         // Build a PreferenceLayout to compare with known ones that are cacheable.
238         mTempPreferenceLayout = createPreferenceLayout(preference, mTempPreferenceLayout);
239 
240         // If it's not one of the cached ones, set the convertView to null so that
241         // the layout gets re-created by the Preference.
242         if (Collections.binarySearch(mPreferenceLayouts, mTempPreferenceLayout) < 0 ||
243                 (getItemViewType(position) == getHighlightItemViewType())) {
244             convertView = null;
245         }
246         View result = preference.getView(convertView, parent);
247         if (position == mHighlightedPosition && mHighlightedDrawable != null) {
248             ViewGroup wrapper = new FrameLayout(parent.getContext());
249             wrapper.setLayoutParams(sWrapperLayoutParams);
250             wrapper.setBackgroundDrawable(mHighlightedDrawable);
251             wrapper.addView(result);
252             result = wrapper;
253         }
254         return result;
255     }
256 
257     @Override
isEnabled(int position)258     public boolean isEnabled(int position) {
259         if (position < 0 || position >= getCount()) return true;
260         return this.getItem(position).isSelectable();
261     }
262 
263     @Override
areAllItemsEnabled()264     public boolean areAllItemsEnabled() {
265         // There should always be a preference group, and these groups are always
266         // disabled
267         return false;
268     }
269 
onPreferenceChange(Preference preference)270     public void onPreferenceChange(Preference preference) {
271         notifyDataSetChanged();
272     }
273 
onPreferenceHierarchyChange(Preference preference)274     public void onPreferenceHierarchyChange(Preference preference) {
275         mHandler.removeCallbacks(mSyncRunnable);
276         mHandler.post(mSyncRunnable);
277     }
278 
279     @Override
hasStableIds()280     public boolean hasStableIds() {
281         return true;
282     }
283 
getHighlightItemViewType()284     private int getHighlightItemViewType() {
285         return getViewTypeCount() - 1;
286     }
287 
288     @Override
getItemViewType(int position)289     public int getItemViewType(int position) {
290         if (position == mHighlightedPosition) {
291             return getHighlightItemViewType();
292         }
293 
294         if (!mHasReturnedViewTypeCount) {
295             mHasReturnedViewTypeCount = true;
296         }
297 
298         final Preference preference = this.getItem(position);
299         if (!preference.canRecycleLayout()) {
300             return IGNORE_ITEM_VIEW_TYPE;
301         }
302 
303         mTempPreferenceLayout = createPreferenceLayout(preference, mTempPreferenceLayout);
304 
305         int viewType = Collections.binarySearch(mPreferenceLayouts, mTempPreferenceLayout);
306         if (viewType < 0) {
307             // This is a class that was seen after we returned the count, so
308             // don't recycle it.
309             return IGNORE_ITEM_VIEW_TYPE;
310         } else {
311             return viewType;
312         }
313     }
314 
315     @Override
getViewTypeCount()316     public int getViewTypeCount() {
317         if (!mHasReturnedViewTypeCount) {
318             mHasReturnedViewTypeCount = true;
319         }
320 
321         return Math.max(1, mPreferenceLayouts.size()) + 1;
322     }
323 
324 }
325