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