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
17package android.preference;
18
19import java.util.ArrayList;
20import java.util.Collections;
21import java.util.List;
22
23import android.graphics.drawable.Drawable;
24import android.os.Handler;
25import android.preference.Preference.OnPreferenceChangeInternalListener;
26import android.view.View;
27import android.view.ViewGroup;
28import android.widget.Adapter;
29import android.widget.BaseAdapter;
30import android.widget.FrameLayout;
31import 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 */
53public 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
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
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
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
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     */
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
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
207    public int getCount() {
208        return mPreferenceList.size();
209    }
210
211    public Preference getItem(int position) {
212        if (position < 0 || position >= getCount()) return null;
213        return mPreferenceList.get(position);
214    }
215
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     */
224    public void setHighlighted(int position) {
225        mHighlightedPosition = position;
226    }
227
228    /**
229     * @hide
230     */
231    public void setHighlightedDrawable(Drawable drawable) {
232        mHighlightedDrawable = drawable;
233    }
234
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
258    public boolean isEnabled(int position) {
259        if (position < 0 || position >= getCount()) return true;
260        return this.getItem(position).isSelectable();
261    }
262
263    @Override
264    public boolean areAllItemsEnabled() {
265        // There should always be a preference group, and these groups are always
266        // disabled
267        return false;
268    }
269
270    public void onPreferenceChange(Preference preference) {
271        notifyDataSetChanged();
272    }
273
274    public void onPreferenceHierarchyChange(Preference preference) {
275        mHandler.removeCallbacks(mSyncRunnable);
276        mHandler.post(mSyncRunnable);
277    }
278
279    @Override
280    public boolean hasStableIds() {
281        return true;
282    }
283
284    private int getHighlightItemViewType() {
285        return getViewTypeCount() - 1;
286    }
287
288    @Override
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
316    public int getViewTypeCount() {
317        if (!mHasReturnedViewTypeCount) {
318            mHasReturnedViewTypeCount = true;
319        }
320
321        return Math.max(1, mPreferenceLayouts.size()) + 1;
322    }
323
324}
325